# Polyglot tasks

**Keep the script, gain the runtime.** A working Python, .NET, or shell step becomes a queued, retried, traced unit of work by declaring it with `defineTask` — the script itself does not change, and nothing about your platform has to stop being TypeScript for one step that is not.

A **polyglot task** runs non-TypeScript work — a Python script, a .NET program, a shell or PowerShell script, or any executable — as a **managed subprocess** spawned by the worker runtime. NetScript hands the task its input as command-line arguments and environment variables, captures every line of `stdout`/`stderr`, parses a final JSON line into a structured result, and normalizes the exit code into a `TaskResult`. It is the escape hatch for the moments your platform is otherwise all-TypeScript: an ML model in Python, a legacy .NET DLL, a system `pwsh` script. alpha

![The worker runtime resolves a TaskDefinition to a runtime adapter, which builds an argv and spawns a python/node/dotnet subprocess; input flows in as args and env, the subprocess streams stdout/stderr back, and the last JSON line of stdout becomes the structured result returned to the queue and database.](https://rickylabs.github.io/netscript/netscript/assets/diagrams/polyglot-task-execution.svg)

*A task is dispatched to a runtime adapter that spawns a subprocess. Input arrives as argv + env; the last JSON line of stdout is parsed into the result; the exit code, captured logs, and duration become a TaskResult.*

The typical story is an import pipeline with one step that already exists in another language: a legacy-data transform owned by a Python script, say, sitting in the middle of an otherwise-TypeScript flow. Rewriting it buys nothing; running it ad hoc loses the queue, retry, and trace behavior every other step has. Declaring it as a task resolves that tension — the [ERP Sync tutorial](https://rickylabs.github.io/netscript/tutorials/erp-sync/03-polyglot-transform/) walks exactly this step, and trace context is injected into the subprocess environment so the Python span stitches into the same trace as the TypeScript steps around it.

## What it is

A **job** runs in-process TypeScript on a worker; a **task** runs a *subprocess* in another runtime. Tasks share the worker plugin's queue, retry, and telemetry machinery with [background jobs](https://rickylabs.github.io/netscript/background-processing/workers/) — the difference is purely the execution surface. The `MultiRuntimeTaskExecutor` keeps a map of **runtime adapters** (one per `TaskType`) and dispatches a `TaskDefinition` to the adapter that supports its `type`. Each adapter builds an `argv` for its runtime (e.g. `python3 -u script.py …`, `pwsh -File script.ps1 …`) and runs it through a Dax-backed process runner that streams output and times the process out. This page covers that subprocess seam; for in-process TS handlers, runtime modes, and the queue lifecycle, start at [background jobs](https://rickylabs.github.io/netscript/background-processing/workers/).

> Use this when
>
> Reach for a polyglot task when the work
>
> must
>
> run in another runtime — a Python data/ML script, a .NET tool, an existing shell or PowerShell automation, or a prebuilt binary — and you want it queued, retried, traced, and result-captured like any other unit of work. If the work is plain TypeScript, use an in-process
>
> background job
>
> instead — it avoids a process spawn entirely.

## Learn → / Do →

[Learn — Workers track  Tutorial Track C builds the worker plugin end to end; lesson 03 adds a polyglot task with explicit permissions.](https://rickylabs.github.io/netscript/netscript/tutorials/erp-sync/)

[Do — Run a polyglot task  Task recipe: define a python (or shell) task, wire its entrypoint and permissions, and execute it through the runtime.](https://rickylabs.github.io/netscript/netscript/background-processing/how-to/run-a-polyglot-task/)

[Understand — Background jobs  The queue, worker-runtime modes, retry, and shutdown machinery that polyglot tasks share with in-process jobs.](https://rickylabs.github.io/netscript/netscript/background-processing/workers/)

## Minimal example

A task is authored with the `defineTask` typestate builder, then run through the default multi-runtime executor. The builder's default `runtime` is `'deno'`; call `.runtime(type)` to target another language. Input reaches the script as **argv** (`.args(...)`) and **environment variables** (`.env({...})`); the script returns a result by writing a single JSON object as the **last line of `stdout`**.

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

// Build a task definition: python runtime, script entrypoint, explicit inputs.
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();

// The executor resolves the python adapter and spawns: python3 -u ./scripts/score.py --threshold 0.8
const executor = createDefaultTaskExecutor();
const result = await executor.execute(scoreBatch, {
  onStdout: (line) => console.log('[score]', line),
});

if (result.success) {
  // result.result is the parsed JSON object from the LAST stdout line, or null.
  console.log('scored', result.result);
} else {
  console.error('task failed', result.exitCode, result.error);
}
```

```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}

# Any prior prints become captured logs. The result is the LAST line of stdout,
# and must be a single JSON object (not an array) to populate result.result.
print('scoring complete', file=sys.stderr)
print(json.dumps(scored))
```

> How input and output cross the process boundary
>
> Input is passed as
>
> command-line arguments
>
> (
>
> args
>
> ) and
>
> environment variables
>
> (
>
> env
>
> ) — there is no JSON-over-stdin channel. The runtime merges
>
> Deno.env
>
> , the task's
>
> env
>
> , and the call's
>
> options.env
>
> , and injects
>
> TRACEPARENT
>
> ,
>
> TRACESTATE
>
> , and
>
> CORRELATION_ID
>
> for trace propagation. Output is the reverse: the subprocess returns a structured value by printing
>
> one JSON object as the final line of `stdout`
>
> ; the runtime parses that line into
>
> result.result
>
> (a non-object or non-final-line value yields
>
> null
>
> ). All other lines are captured into
>
> stdout
>
> /
>
> stderr
>
> and streamed to the
>
> onStdout
>
> /
>
> onStderr
>
> /
>
> onLog
>
> callbacks.

## Key types first — TaskResult

Every task — whatever its runtime — resolves to one `TaskResult`. This is the shape your calling code reads; check it before writing per-runtime branches.

**TaskResult — returned by executor.execute()**

| Name | Type | Description |
| --- | --- | --- |
| `taskId` | `string` | The TaskDefinition.id this result belongs to. |
| `status` | `string` | Lifecycle status: 'completed' \| 'failed' \| 'timeout' \| 'cancelled' (running/pending used in-flight). |
| `success` | `boolean` | True only when status is 'completed' (exit code 0). Branch on this. |
| `exitCode` | `number` | Subprocess exit code; -1 when the process never started (spawn error, cancelled, timeout). |
| `stdout` | `string` | All captured stdout lines, joined by newlines. |
| `stderr` | `string` | All captured stderr lines (and the error message on a spawn failure). |
| `result` | `Record \| null` | The JSON object parsed from the LAST line of stdout, or null if that line is not a JSON object. |
| `error` | `string \| null` | Human-readable failure message (includes exit code and first stderr line); null on success. |
| `duration` | `number` | Wall-clock execution time in milliseconds. |
| `startedAt / completedAt` | `string` | ISO-8601 timestamps bracketing the run. |
| `attempt` | `number` | Attempt index for this execution (0 from the executor itself). |

## TaskDefinition & runtimes

`defineTask(id)` returns a typestate builder; `.build()` is only callable once an `.entrypoint(path)` (subprocess) or `.handler(fn)` (in-process) is set. The resulting `TaskDefinition` carries the runtime `type`, entrypoint, args, env, timeout, and permissions. The seven supported runtimes (`TaskType`) and how each is launched:

**TaskType — built-in runtime adapters**

| Name | Type | Description |
| --- | --- | --- |
| `deno` | `default` | Spawns `deno run` with permission flags built from .permissions(...). The only runtime that is sandboxed by per-task Deno permissions. |
| `python` | `subprocess` | Spawns `python3 -u ` (or `py` on Windows); honors metadata.pythonConfig.venvPath / pythonPath and NETSCRIPT_PYTHON_PATH. |
| `dotnet` | `subprocess` | Runs a .cs via `dotnet run`, a project via `dotnet run --project`, or a built executable directly; metadata.dotnetConfig.runtimeArgs/useDotnetRun. |
| `shell` | `subprocess` | Runs the entrypoint under bash (metadata.shellConfig.shell / loginShell); resolves Git Bash util paths on Windows. |
| `powershell` | `subprocess` | Spawns `powershell` (Windows) or `pwsh` with -NoProfile -NonInteractive -ExecutionPolicy Bypass -File. |
| `cmd` | `subprocess` | Spawns `cmd.exe /c ` (Windows). |
| `executable` | `subprocess` | Runs the entrypoint directly as a prebuilt binary with the task's args. |

The per-task permission set passed to `.permissions(...)` mirrors the Deno permission model. **It is enforced only for the `deno` runtime** — those keys are translated into `--allow-*` flags on the `deno run` command line. For `python`, `shell`, and the other external runtimes there is no Deno sandbox; the subprocess inherits the OS-level access of the worker process.

**permissions() options — applied to the deno runtime**

| Name | Type | Description |
| --- | --- | --- |
| `net` | `boolean \| string[]` | --allow-net (true) or --allow-net=HOSTS (array). Network access. |
| `read` | `boolean \| string[]` | --allow-read or --allow-read=PATHS. Filesystem read. |
| `write` | `boolean \| string[]` | --allow-write or --allow-write=PATHS. Filesystem write. |
| `env` | `boolean \| string[]` | --allow-env or --allow-env=VARS. Environment-variable access. |
| `run` | `boolean \| string[]` | --allow-run or --allow-run=CMDS. Subprocess spawn. |
| `ffi` | `boolean` | --allow-ffi. FFI access to native libraries. |
| `import` | `string[]` | --allow-import=SPECIFIERS. Allowed dynamic-import sources. |

> Omitting permissions widens the Deno sandbox
>
> For a
>
> deno
>
> task, calling
>
> .build()
>
> without
>
> .permissions(...)
>
> produces an
>
> --allow-all
>
> command line. Always pass an explicit, least-privilege permission set for untrusted or third-party Deno task code. Non-Deno runtimes ignore these keys entirely — gate those at the OS level instead.

## Production notes

> The result contract is the LAST stdout line
>
> A subprocess returns structured data
>
> only
>
> by printing a single JSON
>
> object
>
> as its final
>
> stdout
>
> line. A trailing log line, a pretty-print, a JSON
>
> array
>
> , or a stray newline after the payload all cause
>
> result.result
>
> to be
>
> null
>
> even though the task
>
> succeeded
>
> . Print diagnostics to
>
> stderr
>
> , and emit the JSON result last with no trailing output. Buffering can also reorder this — for Python the runtime runs
>
> python3 -u
>
> (unbuffered) for exactly this reason; flush your own output in other runtimes.

> Runtimes must exist on the host; exit codes are diagnostic
>
> Each non-Deno runtime requires its toolchain on the
>
> worker host
>
> : a Python interpreter (or a configured venv), the .NET SDK,
>
> pwsh
>
> /
>
> powershell
>
> , or
>
> bash
>
> . A missing interpreter surfaces as a failed task — exit code
>
> 127
>
> is reported as
>
> command not found
>
> and
>
> 126
>
> as
>
> command not executable
>
> .
>
> cmd
>
> and
>
> powershell.exe
>
> are Windows-only;
>
> shell
>
> uses bash and resolves Git Bash utility paths on Windows. A task that exceeds its
>
> timeout
>
> resolves with status
>
> timeout
>
> and exit code
>
> -1
>
> ; passing an already-aborted
>
> signal
>
> yields
>
> cancelled
>
> .

> Tasks run external code — treat them as a trust boundary
>
> Polyglot tasks spawn arbitrary processes with the worker's OS privileges. Pin entrypoints to known scripts, prefer a pinned interpreter / venv (
>
> pythonConfig.venvPath
>
> ) over
>
> $PATH
>
> discovery, and avoid interpolating untrusted input into
>
> args
>
> or the entrypoint path. Trace context (
>
> TRACEPARENT
>
> /
>
> CORRELATION_ID
>
> ) is injected into the subprocess environment so cross-runtime spans stitch together in telemetry.

## Reference →

The full generated API — `defineTask`, `MultiRuntimeTaskExecutor`, `createDefaultTaskExecutor`, every runtime adapter, and the executor option types — lives in the workers reference.

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

[Look up — Workers reference  defineTask, the multi-runtime executor, runtime adapters, TaskDefinition / TaskResult / TaskExecutionOptions, and permission types.](https://rickylabs.github.io/netscript/netscript/reference/workers/)

[Do — Run a polyglot task  Step-by-step recipe for a python and a shell task with explicit permissions and result capture.](https://rickylabs.github.io/netscript/netscript/background-processing/how-to/run-a-polyglot-task/)

[Understand — Background jobs  The queue, worker-runtime modes, retry, and graceful-shutdown drain shared with in-process jobs.](https://rickylabs.github.io/netscript/netscript/background-processing/workers/)

[Next — Runtime configuration  How worker, service, and adapter settings resolve from schema defaults, config files, env vars, and overrides.](https://rickylabs.github.io/netscript/netscript/orchestration-runtime/runtime-config/)

[Typed SDK & client](https://rickylabs.github.io/netscript/netscript/services-sdk/sdk/) [Runtime configuration](https://rickylabs.github.io/netscript/netscript/orchestration-runtime/runtime-config/)
