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 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 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 — 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.
Learn → / Do →
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.
// 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);
}
# 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))
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.
| 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 |
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:
| 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.
| 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. |
Production notes
Reference →
The full generated API — defineTask, MultiRuntimeTaskExecutor,
createDefaultTaskExecutor, every runtime adapter, and the executor option types — lives
in the workers reference.