Polyglot tasks
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
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.