Skip to main content
0.0.x

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

What you need before defining a task
NameTypeDescription
@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, or a bounded stdin payload.
(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 in this example.
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"

2a. Send private input on stdin

Use .stdin(bytesOrJson) for a fixed task payload or options.stdin for input supplied at execution time. The execution option replaces the builder payload. JSON is encoded as UTF-8; Uint8Array values are sent unchanged. The runner writes the payload with backpressure and closes stdin, so the script can read to EOF. Absent input uses null stdin and never inherits input from the worker host. An explicitly supplied undefined payload fails with MissingStdinPayload.

import { defineTask } from '@netscript/plugin-workers-core/builders';
import { createDefaultTaskExecutor } from '@netscript/plugin-workers-core/executor';

const task = defineTask('private-input')
  .runtime('python')
  .entrypoint('./scripts/private-input.py')
  .stdin({ uid: 'service-operator', roots: ['workspace-root'] })
  .build();
const executor = createDefaultTaskExecutor();
const result = await executor.execute(task, {
  stdin: { uid: 'another-operator', roots: ['another-root'] },
  stdoutLimitBytes: 65_536,
  stderrLimitBytes: 65_536,
});
console.log(result.success);
# scripts/private-input.py
import json, sys

payload = json.load(sys.stdin)
# Use private input without putting it in argv, env, or diagnostics.
print(json.dumps({'accepted': len(payload['roots'])}))

Stdin is capped at 1 MiB of encoded bytes, including JSON punctuation and escaping. Builder input is validated and snapshotted when .stdin() is called; each .build() receives its own byte buffer, which the adapter passes directly to the runner. Execution input is validated before spawning. Oversized input produces StdinPayloadTooLarge. JSON accepts finite numbers, strings, booleans, null, arrays, and plain objects with at most 64 levels of nesting; cycles, accessors, and unsupported values produce InvalidStdinPayload. A subprocess that closes stdin before accepting the payload produces StdinWriteFailed.

By default, stdout and stderr each retain only their last 1 MiB while continuing to read and stream all output. Healthy tasks can print more than 1 MiB without failing; a final stdout JSON object that fits in the retained tail remains available. Lines longer than 1 MiB of characters are delivered to log callbacks in bounded fragments. Set positive integer stdoutLimitBytes / stderrLimitBytes execution options, or configure them on new ExecutableRuntimeAdapter({ stdoutLimitBytes, stderrLimitBytes }), to opt into a total output cap. Exceeding an explicit cap terminates the process tree and returns status: 'failed', success: false, and StdoutLimitExceeded or StderrLimitExceeded; explicit caps also apply with streamLogs: false. Runtime validation and write errors return failed task results; builder validation throws before building the task. Running aborts return cancelled, and timeouts return timeout, both with exit code -1. Subprocesses inherit the worker's process group so terminal and supervisor signals also reach active tasks.

Builder .stdin() applies only to direct executor calls. KV task registration validates through a schema that strips this runtime field, and queue dispatch does not carry it. Supply private per-execution input in options.stdin when calling the executor in a worker. Stdin is not a CLI stdin flag. Scripts control their own stdout/stderr: do not print private payloads.

3. Run it through the workers CLI

ns-workers run-task resolves the generated task metadata and delegates to the same MultiRuntimeTaskExecutor used by the worker runtime. Pass argv and environment as JSON; stdout and stderr stream while the subprocess runs, followed by the complete TaskResult.

ns-workers run-task score-batch \
  --args='["--threshold","0.8"]' \
  --env='{"MODEL_PATH":"./models/scorer.pkl"}' \
  --timeout=120000 \
  --json

The programmatic createDefaultTaskExecutor() API remains available for runtime integrations and custom adapters; ordinary task execution no longer needs a hand-written runner module.

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

In-production pitfalls

See also

Polyglot tasks

Background jobs

workers