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
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
| 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.
// 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
// 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
// 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.
# 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))
#!/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 executor
createDefaultTaskExecutor() builds a MultiRuntimeTaskExecutor wired with every built-in
runtime adapter. Call executor.execute(task, options?) to resolve the matching adapter,
spawn the subprocess, and get back one TaskResult. Per-call options (a
TaskExecutionOptions) can add args, env, a timeout, an AbortSignal, a
correlationId, and the onStdout/onStderr/onLog streaming callbacks.
// workers/run-score.ts
import { createDefaultTaskExecutor } from '@netscript/plugin-workers-core/executor';
import { scoreBatch } from './tasks/score-batch.ts';
const executor = createDefaultTaskExecutor();
const result = await executor.execute(scoreBatch, {
// options.env merges OVER the task's env; trace headers are injected automatically.
onStdout: (line) => console.log('[score]', line),
onStderr: (line) => console.warn('[score:err]', line),
});
if (result.success) {
// result.result is the parsed JSON object from the LAST stdout line, or null.
console.log('scored', result.result, `in ${result.duration}ms`);
} else {
// status is 'failed' | 'timeout' | 'cancelled'; exitCode is -1 when the process never ran.
console.error('task failed', result.status, result.exitCode, result.error);
}
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.
// 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