# Run a polyglot task

Define a non-TypeScript script — a Python program, a shell script, a .NET tool, or any executable — as a NetScript task, give it a permission sandbox, and run it through the multi-runtime executor with its `stdout`/`stderr` captured and its result parsed.

alpha

This is the task-focused **DO** companion to the [polyglot tasks](https://rickylabs.github.io/netscript/capabilities/polyglot-tasks/) hub — read that first for the WHY (what a task is, how the subprocess seam works, the full `TaskResult` shape). This page wires one up.

## Prerequisites

**What you need before defining a task**

| Name | Type | Description |
| --- | --- | --- |
| `@netscript/plugin-workers-core` | `package (alpha)` | Provides defineTask (./builders) and createDefaultTaskExecutor (./executor). |
| `The target toolchain` | `host binary` | The interpreter the task spawns must exist on the worker HOST: python3 (or a venv / py on Windows), bash, pwsh/powershell, the .NET SDK, or your prebuilt binary. |
| `An entrypoint script` | `file path` | The script or executable to run, e.g. ./scripts/score.py. The task passes it input as argv + env, never stdin. |
| `(deno tasks only) a permission set` | `BuilderPermissions` | net/read/write/env/run/ffi/import. Enforced ONLY for the deno runtime — see the sandbox note below. |

## Steps

### 1. Define the task

`defineTask(id)` returns a typestate builder; the default `runtime` is `'deno'`, so call `.runtime(type)` to target another language. Input reaches the script as **argv** (`.args(...)`) and **environment variables** (`.env({...})`). `.build()` is only callable once an `.entrypoint(path)` (subprocess) or `.handler(fn)` (in-process) has been set.

```ts
// workers/tasks/score-batch.ts
import { defineTask } from '@netscript/plugin-workers-core/builders';

// python runtime, script entrypoint, explicit inputs.
export const scoreBatch = defineTask('score-batch')
  .runtime('python')
  .entrypoint('./scripts/score.py')
  .env({ MODEL_PATH: './models/scorer.pkl' })
  .args('--threshold', '0.8')
  .timeout(120_000) // ms; defaults to 300_000
  .build();

// Spawns: python3 -u ./scripts/score.py --threshold 0.8
```

```ts
// workers/tasks/rotate-logs.ts
import { defineTask } from '@netscript/plugin-workers-core/builders';

// shell runtime runs the entrypoint under bash (no -c).
export const rotateLogs = defineTask('rotate-logs')
  .runtime('shell')
  .entrypoint('./scripts/rotate-logs.sh')
  .args('--keep', '7')
  .timeout(30_000)
  .build();

// Spawns: bash ./scripts/rotate-logs.sh --keep 7
```

```ts
// workers/tasks/score-batch.ts — prefer a pinned venv over $PATH discovery
import { defineTask } from '@netscript/plugin-workers-core/builders';

export const scoreBatch = defineTask('score-batch')
  .runtime('python')
  .entrypoint('./scripts/score.py')
  // Resolution order: pythonConfig.pythonPath -> venvPath -> NETSCRIPT_PYTHON_PATH -> python3/py
  .metadata({ pythonConfig: { venvPath: './.venv' } })
  .build();
```

### 2. Write the script so its result crosses the process boundary

The subprocess returns structured data by printing **one JSON object as the last line of `stdout`**. Everything else on `stdout`/`stderr` is captured as logs. Print diagnostics to `stderr`; emit the JSON result last, with no trailing output.

```python
# scripts/score.py
import json, os, sys

# Input arrives as argv + env (NOT stdin).
threshold = float(sys.argv[sys.argv.index('--threshold') + 1])
model_path = os.environ['MODEL_PATH']

# ... do the work ...
scored = {'kept': 42, 'dropped': 3, 'threshold': threshold}

# Diagnostics go to stderr; the result is the LAST stdout line and must be a
# single JSON OBJECT (not an array) to populate result.result.
print('scoring complete', file=sys.stderr)
print(json.dumps(scored))
```

```bash
#!/usr/bin/env bash
# scripts/rotate-logs.sh
set -euo pipefail

keep="${2:-7}"
# ... rotate ...
echo "rotated logs, keeping ${keep}" >&2   # diagnostics -> stderr

# Last stdout line = JSON object result. Non-python runtimes are not -u'd, so
# flush by emitting the JSON as the final write with nothing after it.
printf '{"rotated": true, "kept": %s}\n' "$keep"
```

### 3. Run it through the workers CLI

> ns-workers is a shorthand you install once
>
> ns-workers
>
> is a name
>
> you
>
> give the workers plugin's CLI — the scaffold does not create it. Install it once, globally, and every
>
> ns-workers
>
> command on this page works as written:
>
> ```bash
> deno install -gArf -n ns-workers jsr:@netscript/plugin-workers@0.0.1-beta.11/cli
> ```
>
> Rather not install it? Each
>
> ns-workers <verb …>
>
> is exactly
>
> deno x -A jsr:@netscript/plugin-workers@0.0.1-beta.11/cli <verb …>
>
> — run that full form instead.

`ns-workers run-task` resolves the generated task metadata and delegates to the same `MultiRuntimeTaskExecutor` used by the worker runtime. Pass argv and environment as JSON; stdout and stderr stream while the subprocess runs, followed by the complete `TaskResult`.

```sh
ns-workers run-task score-batch \
  --args='["--threshold","0.8"]' \
  --env='{"MODEL_PATH":"./models/scorer.pkl"}' \
  --timeout=120000 \
  --json
```

The programmatic `createDefaultTaskExecutor()` API remains available for runtime integrations and custom adapters; ordinary task execution no longer needs a hand-written runner module.

### 4. Sandbox a `deno` task with explicit permissions

The `deno` runtime is the **only** one NetScript sandboxes: the `.permissions({...})` set is translated into `--allow-*` flags on the `deno run` command line. For `python`, `shell`, `powershell`, `cmd`, `dotnet`, and `executable`, those keys are ignored — the subprocess inherits the worker process's OS-level access. Gate those at the OS layer instead.

```ts
// workers/tasks/parse-feed.ts — a sandboxed deno task (least privilege)
import { defineTask } from '@netscript/plugin-workers-core/builders';

export const parseFeed = defineTask('parse-feed')
  .runtime('deno') // the default; shown here for clarity
  .entrypoint('./scripts/parse-feed.ts')
  .permissions({
    net: ['api.example.com'], // -> --allow-net=api.example.com
    read: ['./feeds'],        // -> --allow-read=./feeds
    write: false,
    env: ['FEED_TOKEN'],      // -> --allow-env=FEED_TOKEN
  })
  .build();

// Spawns: deno run --allow-net=api.example.com --allow-read=./feeds --allow-env=FEED_TOKEN ./scripts/parse-feed.ts
```

> Omitting permissions on a deno task widens the sandbox to --allow-all
>
> Calling
>
> .build()
>
> on a
>
> deno
>
> task
>
> without
>
> .permissions(...)
>
> produces an
>
> --allow-all
>
> command line — full access. Always pass an explicit, least-privilege permission set for untrusted or third-party Deno task code. The
>
> permissions
>
> preset export (
>
> permissions.readOnly
>
> ,
>
> .network
>
> ,
>
> .none
>
> , …) gives you ready-made starting points.

## In-production pitfalls

> The result is the LAST stdout line — and only if it is a JSON object
>
> A trailing log line, a pretty-print, a JSON
>
> array
>
> , or a stray newline after the payload all make
>
> result.result
>
> null
>
> even though
>
> result.success
>
> is
>
> true
>
> . Emit the JSON object last, send everything else to
>
> stderr
>
> , and flush your own output — for Python the runtime runs
>
> python3 -u
>
> (unbuffered) for exactly this reason; other runtimes are not unbuffered.

> The runtime must exist on the worker host; read the exit code
>
> A missing interpreter surfaces as a failed task, not a thrown error. Exit code
>
> 127
>
> is reported as
>
> command not found
>
> and
>
> 126
>
> as
>
> command not executable
>
> ; both land in
>
> result.error
>
> with the first
>
> stderr
>
> line appended.
>
> cmd
>
> and (on Windows)
>
> powershell
>
> are platform-specific. A task that exceeds its
>
> timeout
>
> resolves with status
>
> timeout
>
> and exit code
>
> -1
>
> ; passing an already-aborted
>
> signal
>
> yields status
>
> cancelled
>
> before any process starts.

> Non-Deno runtimes are a trust boundary — there is no per-task sandbox
>
> A
>
> python
>
> /
>
> shell
>
> /
>
> executable
>
> task spawns an arbitrary process with the worker's OS privileges;
>
> .permissions(...)
>
> does nothing there. Pin entrypoints to known scripts, prefer a pinned interpreter or venv (
>
> pythonConfig.venvPath
>
> ) over
>
> $PATH
>
> discovery, and never interpolate untrusted input into
>
> args
>
> or the entrypoint path. Trace context (
>
> TRACEPARENT
>
> /
>
> TRACESTATE
>
> /
>
> CORRELATION_ID
>
> ) is injected into the subprocess environment so cross-runtime spans stitch together; env precedence is
>
> Deno.env
>
> < task
>
> env
>
> < call
>
> options.env
>
> .

> Alpha surface
>
> NetScript is in alpha; the polyglot task API can still shift. Treat the import subpaths (
>
> /builders
>
> ,
>
> /executor
>
> ) and the runtime-specific
>
> metadata
>
> configs as the current shape, and pin the version you build against.

## See also

[Polyglot tasks](https://rickylabs.github.io/netscript/netscript/background-processing/polyglot-tasks/)

[Background jobs](https://rickylabs.github.io/netscript/netscript/background-processing/workers/)

[workers](https://rickylabs.github.io/netscript/netscript/reference/workers/)

[Tune the worker runtime](https://rickylabs.github.io/netscript/netscript/background-processing/how-to/tune-worker-runtime/) [Graceful shutdown](https://rickylabs.github.io/netscript/netscript/orchestration-runtime/how-to/graceful-shutdown/)
