# Quickstart

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

Source: https://docs.getrunstate.com/quickstart/

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.

## 1. Get a space and an API key

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

1. Sign in to the console at [app.getrunstate.com](https://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](https://getrunstate.com/request-access).

## 2. Install the SDK

**TypeScript**

Node.js 22 or later.

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

**Python**

Python 3.10 or later. The package installs as `runstate-sdk` and imports as `runstate`.

```bash
mkdir runstate-quickstart && cd runstate-quickstart
python -m venv .venv && source .venv/bin/activate
pip install runstate-sdk
```

## 3. Point the SDK at the hosted API

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

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

> **Caution: Set the base URL**
>
> If `RUNSTATE_BASE_URL` is not set (and you don't pass `baseURL` / `base_url`), the SDKs connect to `http://localhost:8080`. Against the hosted service that shows up as `UnavailableError` after a few seconds.

## 4. Write the producer

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.

**TypeScript**

`producer.ts`

```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);
```

**Python**

`producer.py`

```python
import asyncio
import time

from runstate import AsyncRunstate

async def main() -> None:
    async with AsyncRunstate(holder="quickstart-producer") as rs:
        # 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.
        run = await rs.scopes.create()
        print(f"RUNSTATE_SCOPE_ID={run.id}", flush=True)

        # 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.
        ticket = await run.mailbox("work").submit(
            {"greeting": "runstate", "n": 21},
            key=f"quickstart-task-{int(time.time())}",
        )
        print(f"task {ticket.id} submitted, waiting for a worker...", flush=True)

        # Poll until the task has a recorded result.
        outcome = await ticket.result(timeout_ms=60_000)
        print(outcome["state"], outcome["outcome"])

asyncio.run(main())
```

## 5. Write the worker

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.

**TypeScript**

`worker.ts`

```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})`);
});
```

**Python**

`worker.py`

```python
import asyncio
import os

from runstate import AsyncRunstate

async def main() -> None:
    run_id = os.environ["RUNSTATE_SCOPE_ID"]  # the id the producer printed
    async with AsyncRunstate(holder="quickstart-worker") as rs:
        queue = rs.scope(run_id).mailbox("work")
        print(f"worker listening on run {run_id}", flush=True)

        async def handle(data, delivery):
            # Completing with a payload records it as the task's result.
            await delivery.complete(result={"payload": {"doubled": data["n"] * 2}})
            print(f"completed {delivery.id} (attempt {delivery.attempt})", flush=True)

        # Runs until the process is stopped.
        await queue.consume(handle)

asyncio.run(main())
```

## 6. Run it

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

**TypeScript**

```bash
npx tsx producer.ts
```

**Python**

```bash
python producer.py
```

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

**TypeScript**

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

**Python**

```bash
RUNSTATE_SCOPE_ID=<id printed by the producer> python worker.py
```

The worker completes the task and the producer prints:

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

## What just happened

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

## Next steps

- [Concepts](https://docs.getrunstate.com/concepts/): the vocabulary behind runs, work queues, tasks and leases.
- [Claims, shared tasks and takeover](https://docs.getrunstate.com/guides/work-once/): ownership of keys, shared tasks, and crash takeover in detail.
- [Configuration & auth](https://docs.getrunstate.com/configuration/): every client option, key permissions and worker identity.
