Skip to content
HomeConsoleGet started

Submit a durable task from one process, complete it from a worker in another, and read back its recorded result, in TypeScript or Python.

In this quickstart you run two programs against the hosted service:

  • a producer that opens a run, submits one task to a work queue, and waits for the result;
  • a worker that takes tasks from that queue and completes them.

The producer and worker can be in different languages. Pick a tab once and every code sample on the site follows it.

runstate is in a private developer preview. Once you have access:

  1. Sign in to the console at app.getrunstate.com and open a space (a project or environment; the API calls it a space). Copy its space id.
  2. Open API keys in that space and create a key. The secret is shown once, so copy it now.

No access yet? Request preview access.

Node.js 22 or later.

mkdir runstate-quickstart && cd runstate-quickstart
npm init -y
npm pkg set type=module
npm install runstate-sdk
npm install --save-dev tsx

Both SDKs read three environment variables. Set them in every terminal you use below:

export RUNSTATE_BASE_URL=https://api.getrunstate.com
export RUNSTATE_SPACE_ID=<your space id>
export RUNSTATE_API_KEY=<your api key>

The producer makes sure a work queue named work exists, opens a run, submits one task, and waits up to 60 seconds for its result.

producer.ts
import { Runstate } from 'runstate-sdk';
const rs = new Runstate({ holder: 'quickstart-producer' });
// Create the work queue if it doesn't exist yet (safe to call every time).
await rs.mailboxes.ensure('work', { mode: 'WORK' });
// A run groups everything this swarm does, so you can cancel it as one unit.
const run = await rs.scopes.create();
console.log(`RUNSTATE_SCOPE_ID=${run.id}`);
// Submit a task. The key identifies the logical task: submitting the same
// key with the same input again returns this task instead of a new one.
const ticket = await run.mailbox('work').submit(
{ greeting: 'runstate', n: 21 },
{ key: `quickstart-task-${Date.now()}` },
);
console.log(`task ${ticket.id} submitted, waiting for a worker...`);
// Poll until the task has a recorded result.
const outcome = await ticket.result({ timeoutMs: 60_000 });
console.log(outcome.state, outcome.outcome);

The worker attaches to the producer’s run and consumes the work queue. consume() keeps the task’s lease alive while your handler runs, completes the delivery when the handler returns, and puts it back on the queue if the handler throws.

worker.ts
import { Runstate } from 'runstate-sdk';
const runId = process.env.RUNSTATE_SCOPE_ID;
if (!runId) throw new Error('set RUNSTATE_SCOPE_ID to the id the producer printed');
const rs = new Runstate({ holder: 'quickstart-worker' });
const queue = rs.scope(runId).mailbox('work');
console.log(`worker listening on run ${runId}`);
// Runs until the process is stopped.
await queue.consume<{ greeting: string; n: number }>(async (data, delivery) => {
// Completing with a payload records it as the task's result.
await delivery.complete({ payload: { doubled: data.n * 2 } });
console.log(`completed ${delivery.id} (attempt ${delivery.attempt})`);
});

Start the producer in one terminal. It prints the run id, then waits:

npx tsx producer.ts

Within 60 seconds, start the worker in a second terminal with that id:

RUNSTATE_SCOPE_ID=<id printed by the producer> npx tsx worker.ts

The worker completes the task and the producer prints:

SUCCEEDED { doubled: 42 }

(Python prints SUCCEEDED {'doubled': 42}.) Stop the worker with Ctrl-C.

The two sides are interchangeable: a TypeScript producer works with a Python worker and the other way round, because both talk to the same API.

  • The run (rs.scopes.create()) is the unit you cancel to stop new work. Workers only receive tasks submitted under the run they attach to, which is why the worker needs the run id.
  • The task is durable. If the producer crashed while waiting, it could re-attach later with rs.scope(runId).task(taskId) and call result() again. The recorded result is retrieved, not recomputed.
  • The lease. The worker held the task under a lease that consume() renewed while the handler ran. Had the worker died mid-task, the lease would have expired and another worker would have received the same task, with attempt incremented. A late completion from the dead worker is rejected.
  • The result is exactly one recorded outcome per task. The work itself can run more than once if a worker crashes after doing it but before completing, so make side effects safe to repeat.