Skip to content

Toren Quickstart ​

From zero to a durable multi-agent run, locally with one dependency, then the identical agent in your own AWS account.

Everything below runs today, offline out of the box, and the AWS path has survived a live kill test on a real account (worker killed mid-run, resumed, zero tokens re-paid).


1. Install & scaffold ​

bash
npx toren-run init research-crew
cd research-crew
npm install                 # runtime + @toren-run/core for your tools

You get a filesystem-first agent: files, not framework config.

research-crew/
  package.json          # toren + @toren-run/core, ready to install
  docker-compose.yml    # the one dependency: Postgres
  agent.yaml            # model, limits, budget caps
  instructions.md       # the system prompt
  workflow.ts           # how work fans out (waves)
  tools/
    search-web.ts       # you'll add send-report.ts in step 2
  subagents/
    researcher/         # a nested agent, same layout
    writer/

2. Define a tool ​

Tools are plain TypeScript with three durability attributes: what the tool touches (effects), whether retries are safe (idempotency), and whether a human must sign off (approval):

ts
// tools/send-report.ts
import { defineTool } from "@toren-run/core";
import { z } from "zod";

export default defineTool({
  name: "send_report",
  description: "Email the finished report.",
  input: z.object({ to: z.string(), body: z.string() }),
  effects: "external",      // recorded once; never re-executed on resume
  idempotency: "keyed",     // retries carry the same idempotency key
  approval: "always",       // run parks (zero compute) until a human approves
  handler: async ({ to, body }) => {
    /* your send logic */
    return `sent to ${to}`;
  },
});

Need an API key in a tool? Declare it; toren validates at startup and hands it to handlers as ctx.env, never storing it:

yaml
# agent.yaml
env:
  required: [SERP_API_KEY]

3. Define the workflow ​

The workflow is a short, deterministic script. Parallelism comes from waves: dispatch a batch of agent tasks, get their results, decide what's next:

ts
// workflow.ts
import type { WorkflowCtx } from "@toren-run/core";

export default async function (ctx: WorkflowCtx) {
  const topics = JSON.parse(ctx.input) as string[];

  // Wave 1: N researchers in parallel
  const research = await ctx.wave(
    "research",
    topics.map((t) => ctx.task("researcher", t)),
    { onTaskFailure: "collect" },   // failures are reported, not fatal
  );

  // Plain code between waves, filter, branch, iterate
  const found = research.results.filter((r) => r.status === "completed");

  // Wave 2: one writer over the combined findings
  const summary = await ctx.wave("summarize", [
    ctx.task("writer", found.map((r) => r.output).join("\n")),
  ]);

  return summary.results[0].output ?? "";
}

4. Run it locally ​

The whole local stack is Postgres. Nothing else.

bash
docker compose up -d db     # postgres, the whole local stack
npx toren run . --input '["solar shipping","battery freight"]'
run 7f3a2c10-9b1e-4f6a-8c2d-5e9012ab34cd  agent research_crew  started
run 7f3a2c10-9b1e-4f6a-8c2d-5e9012ab34cd  completed
echo(echo(solar shipping)
echo(battery freight))

(Run ids are UUIDs; per-wave progress lives in toren jobs show <runId> and the console. The offline mock/echo model wraps every task's input in echo(...); swap model: in agent.yaml to anthropic/claude-opus-5 or openai/gpt-4o for real output.)

Programmatic equivalent (works today):

ts
import { LocalWorkerRuntime, startRun } from "@toren-run/core";

const runId = await startRun(deps, { agent: "research-crew", input: JSON.stringify(topics) });
new LocalWorkerRuntime(deps).start();   // pollers for orchestration + tasks

5. The part that feels like magic ​

Kill the process mid-run. Hard. One prep step: mock/echo answers too fast to murder, so switch model: in agent.yaml to mock/slow first (the same offline echo at three seconds per call; no API key needed).

bash
npx toren run . --input '["solar shipping","battery freight"]' &
sleep 10 && kill -9 %1        # murder it mid-wave
npx toren dev                  # bring the stack back

The run resumes at the exact step it died on. Every completed model call is replayed from the event log, not re-executed. A resumed run re-pays zero tokens for finished work. This isn't best-effort: the test suite kills the stack after every single write point in a run and asserts each LLM step was paid for exactly once.

Edit a prompt mid-flight? Only the steps your edit actually affects re-run; everything unchanged stays cached (each recorded step carries a digest of its exact request, verified on replay).

6. Approvals ​

When the writer tries send_report, the run parks durably at zero compute. No worker polling, no idle container.

bash
toren jobs list
#  7f3a2c10-9b1e-4f6a-8c2d-5e9012ab34cd  research_crew  waiting_approval  (send_report)

toren jobs show 7f3a2c10-9b1e-4f6a-8c2d-5e9012ab34cd
#  pending approval: send_report {"to":"board@fund.com"}
#  → toren jobs approve 7f3a2c10-9b1e-4f6a-8c2d-5e9012ab34cd w1t0 s14

toren jobs approve 7f3a2c10-9b1e-4f6a-8c2d-5e9012ab34cd w1t0 s14     # or: --deny --comment "wrong list"

The run wakes, executes the tool once, and continues.

7. Watch it in the console ​

toren dev prints a pre-authenticated link to the built-in web console:

toren console: http://localhost:7433/console/#token=…

Live runs, full event timelines (every model call with its token usage), one-click approve/deny on parked runs, and API-key management. Try the kill test again with the run's timeline open, you can watch it survive.

Serving more than one agent is the same command: toren dev --dir crews/ loads every agent directory in the folder (or repeat --dir), each crew with its own isolated event log. The console shows the whole fleet.

8. Trigger it from anywhere ​

toren dev serves an HTTP API (bearer-token auth), and @toren-run/client wraps it, typed:

ts
import { TorenClient } from "@toren-run/client";

const toren = new TorenClient({ url: process.env.TOREN_URL!, token: process.env.TOREN_TOKEN! });
const { runId } = await toren.startRun({ input: JSON.stringify(topics) });
const run = await toren.waitForRun(runId);          // terminal, or parked on an approval
if (run.status === "waiting_approval") await toren.approve(runId, { ...run.approvals[0], granted: true });

Name your deployments once in .toren/environments.json, then every command targets any of them:

bash
toren run . --input '"hello"' --env staging     # → env: staging (http://…), via the API
toren jobs list --env prod

9. Same agent, your AWS account ​

bash
toren deploy-aws --region eu-central-1 --plan-only   # preview everything it would create
toren deploy-aws --region eu-central-1 --yes         # terraform apply into YOUR account

Locally: Postgres does queue + state + log. In AWS: SQS + Lambda/Fargate + RDS, bound behind the same four interfaces, in your VPC, inside your data boundary. The orchestrator binary is byte-identical in both.

Resetting local state: everything durable lives in that one Postgres. docker compose down -v wipes runs, sessions, schedules, and queues; the next toren command re-migrates a fresh database.


Where it stands: steps 1–8 run today and are chaos- and live-tested (HTTP API, typed SDK, console, environment profiles included). Step 9 is live-validated too: the Terraform module has been applied to real AWS accounts, greenfield and into an existing VPC, and passed the full kill test there: a Fargate worker killed mid-run on real Anthropic billing, resumed by a replacement task, zero duplicate paid calls in the event-log audit.