Skip to content

JavaScript and TypeScript SDK

@faber/runtime-sdk is the TypeScript and JavaScript client for the Faber API v1. It covers every operation, builds requests fluently, runs client-side tests on results, and reports failures through one error type. It uses only fetch, so it runs in Node.js 18+, Bun, Deno, browsers and edge runtimes.

The 0.3 SDK requires a Faber 0.3 service. Upgrading from 0.2 is covered in the migration guide.

Terminal window
npm install @faber/runtime-sdk

With other tools: bun add @faber/runtime-sdk, pnpm add @faber/runtime-sdk, yarn add @faber/runtime-sdk. In Deno, import from npm:@faber/runtime-sdk.

import { FaberClient } from '@faber/runtime-sdk';
const client = new FaberClient({
baseUrl: 'http://localhost:3000',
apiKey: process.env.FABER_API_KEY,
});
OptionDefaultMeaning
baseUrlrequiredService URL, with or without /api/v1. Relative URLs work in browsers.
apiKeynoneSent as Authorization: Bearer <key>.
timeoutMs60000Deadline for a whole call, retries and waits included. 0 disables it.
maxRetries2Retries after a 503, a 429 or a network error.
retryDelayMs500First backoff delay; doubles per retry, with jitter.
maxRetryDelayMs30000Longest wait before a retry. A longer Retry-After ends the retries.
headers{}Extra headers on every request.
fetchglobal fetchAny fetch-compatible function.

Every method takes an optional last argument { signal, timeoutMs, maxRetries, headers } that applies to that call only:

const controller = new AbortController();
const info = await client.info({ signal: controller.signal, timeoutMs: 5_000 });

A request is a list of steps run in order in one sandbox workspace, /faber. A step is a single task, or an array of tasks that run in parallel. Files written by one step are visible to the later ones.

execute(request) sends POST /execute and returns { steps, cache }:

const response = await client.execute({
stop_on_failure: true,
steps: [
{ cmd: 'gcc', args: ['-O2', '-o', 'add', 'add.c'], files: { 'add.c': source }, cache: true },
[
{ cmd: './add', stdin: '1 2\n', sandbox_profile: 'native_v2' },
{ cmd: './add', stdin: '40 2\n', sandbox_profile: 'native_v2' },
],
],
});
response.steps[0]; // the compile result
response.steps[1]; // an array: one result per parallel task
response.cache; // { enabled, hits, stored }

Request fields:

FieldMeaning
stepsTasks and parallel task arrays
cacheDefault for tasks that do not set cache (default false)
stop_on_failureAfter a step where a task did not exit 0, report the remaining steps as not started (default false)

Task fields:

FieldMeaning
cmdProgram; resolved through PATH unless it contains /
args, env, stdinArguments, environment, standard input
filesWorkspace-relative path to file content (see below)
working_dirAbsolute start directory (default /faber)
sandbox_profilecompile_v2, native_v2, compile_v1 or native_v1; the service default otherwise
cacheThe task is deterministic and its step may be cached

A file is a string (UTF-8 text), an inline file { content, encoding: 'utf8' | 'base64', executable }, or a reference to an uploaded file { file_id, executable }.

Runs one task and returns its result:

const result = await client.executeSingle({ cmd: 'echo', args: ['hello'] });

Each step result has the shape of its step. A task result is either completed or failed:

type TaskResult =
| { stdout: string; stderr: string; exit_code: number; stats: TaskStats; cached: boolean }
| { error: string; stats: TaskStats; cached: boolean };

A completed task ran and reported an exit status, which may be non-zero (128 + n when killed by signal n). A failed task never reached one: the command was not found, setup failed, or it was skipped. stats holds memory_peak_bytes, cpu_usage_usec, pids_peak, execution_time_ms, stdout_truncated, stderr_truncated, termination_signal and the outcome:

OutcomeMeaning
exitedExited on its own
signaledKilled by a signal
timed_outHit the wall-clock or CPU time limit
out_of_memoryKilled by its memory limit
ancestor_out_of_memoryKilled by a limit above the task’s own
pids_limitHit the process limit
output_limitProduced more output than allowed
policy_violationMade a syscall its sandbox profile forbids (SIGSYS)
not_startedNever started (stop_on_failure, or the overall deadline passed)
infrastructure_failureFaber could not run it

These are results, not exceptions. Helpers read them:

import { allSucceeded, firstFailure, taskResults, isCompleted, isSuccess, isSkipped } from '@faber/runtime-sdk';
if (!allSucceeded(response)) {
const { stepIndex, taskIndex, result } = firstFailure(response)!;
console.log(stepIndex, taskIndex, result.stats.outcome);
}
for (const { stepIndex, taskIndex, parallel, result } of taskResults(response)) {
if (isCompleted(result)) console.log(stepIndex, taskIndex, result.exit_code);
}

isSuccess means completed with exit status 0; isSkipped means outcome not_started.

TaskBuilder collects steps and the request options; task() builds one task. Both produce the plain objects above.

import { TaskBuilder, task } from '@faber/runtime-sdk';
const plan = new TaskBuilder()
.cache(false)
.stopOnFailure()
.single(
task('gcc', ['-O2', '-o', 'main', 'main.c'])
.file('main.c', source)
.file('data/blob.bin', new Uint8Array([0, 1, 2]))
.executable('run.sh', '#!/bin/sh\nexec ./main\n')
.fileRef('input.txt', fileId)
.cache(),
)
.parallel([
task('./main').stdin('1 2\n').profile('native_v2'),
task('./main').stdin('40 2\n').profile('native_v2').env({ LANG: 'C' }),
])
.single(task('sh', ['-c', 'exit 3']).workingDir('/faber/data'));
const response = await client.execute(plan);
plan.toRequest(); // the request object it sends
TaskBuilder
single(task)Adds a one-task step (a plain task or a task() spec)
parallel(tasks)Adds a parallel step
cache(enabled = true)Sets the request-level cache default
stopOnFailure(enabled = true)Sets stop_on_failure
singleWithTests(task, tests), parallelWithTests(tasks)Steps with client-side tests
build(), toRequest()The steps; the full request
task(cmd, args?)
args(...args), env(vars), stdin(text)Arguments (appended), environment (merged), standard input
file(path, content, { executable })Text, bytes (sent as base64), or an inline file or file reference object
executable(path, content)A file with mode 0755
fileRef(path, fileId, { executable })An uploaded file
workingDir(dir), profile(name), cache(enabled = true)Start directory, sandbox profile, cacheable
test(...tests), build()Client-side tests; the task object

For plain objects, textFile(content, opts), binaryFile(bytes, opts), fileRef(id, opts) and file(content, opts) return file values, and encodeBase64(bytes) is available too.

When the service runs with CACHE_ENABLED=true, a step whose tasks all have cache: true is stored after every task in it exits 0, together with the workspace it leaves behind. The next identical request replays it: the result comes back with cached: true and the original stats, and later steps see the restored files. Only a leading run of cacheable steps is cached, so mark deterministic setup (compiling, installing dependencies) and leave steps that run untrusted input uncached. response.cache.hits and stored count the replayed and newly stored steps; /tmp is not part of a cached layer.

const upload = await client.uploadFile(bytes, { filename: 'input.txt', contentType: 'text/plain' });
// { file_id, size, already_exists }
const { files } = await client.listFiles();
// [{ id, filename, content_type, size, created_at: '2026-10-18T12:00:00Z' }]
const content = await client.downloadFile(upload.file_id); // Uint8Array
await client.deleteFile(upload.file_id);

uploadFile accepts a Blob or File, Uint8Array (including Node Buffer), ArrayBuffer, ReadableStream (read fully before sending) or a string (UTF-8). The filename and content type are sent only when you pass them or the data is a File. Files are stored under the SHA-256 of their content; uploading the same bytes again returns the same id with already_exists: true. Use the id in a task with fileRef.

In Node.js, Bun and Deno, read from disk with the separate /node entry:

import { uploadFileFromPath } from '@faber/runtime-sdk/node';
const { file_id } = await uploadFileFromPath(client, './testdata/input.txt');
MethodEndpointReturns
health()GET /health{ status: 'ok' | 'shutting_down', version }; a draining service answers 503, which resolves rather than throws
info()GET /infoversion, arch, sandbox_profiles { default, allowed }, limits, cache { enabled, backend }, toolchains
clearCache()DELETE /cache{ cleared }; throws cache_disabled when the service has no cache
metrics()GET /metricsPrometheus text
const info = await client.info();
console.log(info.limits.wall_timeout_ms, info.sandbox_profiles.allowed);

Attach tests to tasks, run them with executeWithTests, and summarize them with TestResultAnalyzer. Tests are removed from the request before it is sent.

import { TaskBuilder, TestResultAnalyzer, task } from '@faber/runtime-sdk';
const cases = [
{ input: '1 2\n', expected: '3\n' },
{ input: '40 2\n', expected: '42\n' },
];
const plan = new TaskBuilder()
.stopOnFailure()
.single(
task('gcc', ['-O2', '-o', 'add', 'add.c']).file('add.c', source).cache()
.test({ name: 'compiles', assertion: 'equals', field: 'exit_code', expected: 0 }),
)
.parallel(
cases.map((c, i) =>
task('./add').stdin(c.input).profile('native_v2')
.test({ name: `case ${i + 1}`, assertion: 'equals', field: 'stdout', expected: c.expected }),
),
);
const result = await client.executeWithTests(plan);
const analyzer = new TestResultAnalyzer(result);
console.log(analyzer.formatReport());
analyzer.assertAllPassed();
AssertionFieldsexpected
equalsstdout, stderr, exit_codestring or number
containsstdout, stderrstring
matchesstdout, stderrRegExp
customtestFn: (result) => ({ name, passed, message })

executeWithTests returns the raw results, the cache summary, stepResults (each with passed, skipped and cached), allTestsPassed, passedCount and failedCount. A task that never reached an exit status fails with a task execution test. TestResultAnalyzer provides allPassed, totalSteps, passedSteps, failedSteps, skippedSteps, cachedSteps, getFailedSteps(), getFirstFailure(), getStageFailure(index), getStageSummary(), formatReport({ showPassed, includeOutput }) and assertAllPassed(). Skipped and cached stages are marked in the report.

Every failed call throws a FaberError:

import { FaberError } from '@faber/runtime-sdk';
try {
await client.execute(plan);
} catch (error) {
if (!(error instanceof FaberError)) throw error;
switch (error.code) {
case 'unauthorized':
// check FABER_API_KEY
break;
case 'capacity_exhausted':
case 'shutting_down':
case 'rate_limited':
// already retried; try later (error.retryAfterMs)
break;
default:
console.error(error.status, error.code, error.message, error.requestId);
}
}
FieldMeaning
statusHTTP status; 0 when no response arrived
codeThe API’s error code, or a client code
messageThe server’s message, or a description
requestIdThe x-request-id response header
retryAfterMsThe server’s Retry-After, in milliseconds
rawCodeThe server’s code when the SDK does not know it (code is then internal_error)
body, causeParsed error body; underlying error
retryabletrue for 503, 429 and network errors
CodeStatusWhen
invalid_request400Malformed body or parameters
unauthorized401Missing or wrong API key
not_found404No such file
file_not_found404A task references a file id that is not stored
payload_too_large413Body over the configured limit
limits_exceeded422Too many steps or parallel tasks
profile_not_allowed422Sandbox profile not allowed
cache_disabled409clearCache() without a cache
rate_limited429The API key used up its request allowance
capacity_exhausted503No free execution slots
shutting_down503The service is draining
cancelled503The request was cancelled
deadline_exceeded504The request overran its deadline
sandbox_setup_failed500The sandbox could not be set up
internal_error500Anything else
network_error0The connection failed
timeout0timeoutMs passed
aborted0The caller’s AbortSignal fired
invalid_responseanyA success response had an unexpected shape

Retries. 503 and 429 responses and network errors are retried up to maxRetries times. The client waits for Retry-After when the server sends it, and otherwise backs off exponentially with jitter. Every other 4xx response, other 5xx responses and timeouts are thrown at once. A retry that would not fit within timeoutMs, or a Retry-After longer than maxRetryDelayMs, ends the retries with the last error.

ERROR_CODES, TASK_OUTCOMES and SANDBOX_PROFILES are exported as arrays for building UIs or validating input.

RuntimeUse
Node.js 18+import (ESM) or require (CommonJS)
Bun, DenoESM; the /node entry works too
Browsers, edgeESM, or dist/index.global.js in a script tag (window.Faber)

In a browser, keep the API key on a server you control and call Faber through it, or put Faber behind a proxy that adds the key.

The SDK ships five runnable examples in sdks/js/examples:

ExampleShows
hello-world.tsHealth check and one command
compile-and-run-c.tsCompile C, run it with stdin, report the first failure
cached-test-cases.tsA cached compile step and parallel test cases with a test report
upload-file.tsUpload a file, list it, use it by reference, download and delete it
errors-and-limits.tsFaberError codes; timeout, out-of-memory and policy-violation outcomes
Terminal window
cd sdks/js
npm ci
FABER_URL=http://localhost:3000 FABER_API_KEY=your-key npx tsx examples/cached-test-cases.ts