Skip to main content
Alpha

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

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

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.

TaskResult — returned by executor.execute()
NameTypeDescription
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
NameTypeDescription
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
NameTypeDescription
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.

workers