# Quickstart

To quickly understand kassette, let's build a small durable workflow that can stop and later continue without redoing completed work. It runs locally without any additional infra to install.

The example will model a typical agent flow:

1. Classify a support ticket (using an expensive LLM call).
2. If the ticket is high severity, pause for human review.
3. Draft a response.
4. Return the result.

Without kassette, retrying the workflow would run the expensive call again. You would need to write separate code to store the review result state and load it when waking the workflow. With kassette, you write one async function. Completed steps are recorded in the journal, replay skips them, and `resume()` continues from the suspend point with the review result.

## Prerequisites

- Node.js 24+ (runs TypeScript natively).
- `npm install @usekassette/kassette`.
- An ESM project (`"type": "module"` in `package.json`).

## 1. Write the workflow

```typescript
// workflow.ts
import { kassette, LocalStorage } from '@usekassette/kassette';

type Ticket = { id: string; subject: string; body: string };
type Verdict = { approved: boolean; reviewer: string };
type Events = { 'manual-review': Verdict };

export const triage = kassette<Ticket, string, Events>(
  async (ctx, ticket) => {
    const classification = await ctx.step('classify', async () => {
      console.log(`[${ticket.id}] LLM classify (memoize me)`);
      await new Promise((r) => setTimeout(r, 500));
      return { category: 'bug', severity: 5 };
    });

    let note: string | undefined;
    if (classification.severity >= 4) {
      const verdict = await ctx.suspend('manual-review', {
        reason: `severity ${classification.severity}`,
      });
      if (!verdict.approved) return `rejected by ${verdict.reviewer}`;
      note = `approved by ${verdict.reviewer}`;
    }

    return await ctx.step('draft', async () => {
      console.log(`[${ticket.id}] LLM draft`);
      await new Promise((r) => setTimeout(r, 500));
      return `Triaged as ${classification.category}. ${note ?? ''}`;
    });
  },
  { storage: new LocalStorage('.kassette') },
);
```

You can hand kassette just an ordinary function with an ordinary control flow like `if`, `return`, `async`/`await`. `ctx.step()` will journal the return value so that on replay it can use that memoized value rather than re-running it. `ctx.suspend()` exits the run with a suspended status so that the next `resume()` replays up to the suspend point and then continues with the event payload from there.

## 2. Run it

```typescript
// start.ts
import { createRunId } from '@usekassette/kassette';
import { triage } from './workflow.ts';

const runId = createRunId();
const result = await triage.start({ id: 't1', subject: 'broken', body: '500s on /orders' }, { runId });

console.log(JSON.stringify(result, null, 2));
console.log(`runId: ${runId}`);
```

```bash
$ node start.ts
[t1] LLM classify (memoize me)
{
  "status": "suspended",
  "event": "manual-review",
  "runId": "..."
}
runId: ...
```

The process exits cleanly. The LLM call ran once and was journaled. Now the run is paused waiting for the `manual-review` event.

## 3. Inspect the journal

```bash
$ cat .kassette/<runId>.jsonl | jq
{"type":"start","metadata":{"id":"t1", ...},"timestamp":"...","session":1,"offset":0}
{"type":"step","name":"classify","result":{"category":"bug","severity":5}, ...}
{"type":"suspend","waitingFor":"manual-review","reason":"severity 5", ...}
```

## 4. Resume

When we're ready, we can resume the run. `resume` will normally be triggered by an external event from a webhook handler, a Slack action, a CLI command, etc.

```typescript
// resume.ts
import { triage } from './workflow.ts';

const runId = process.argv[2]!;
const result = await triage.resume(runId, {
  eventName: 'manual-review',
  value: { approved: true, reviewer: 'alice' },
});
console.log(JSON.stringify(result, null, 2));
```

```bash
$ node resume.ts <runId>
[t1] LLM draft
{
  "status": "success",
  "result": "Triaged as bug. approved by alice",
  "runId": "..."
}
```

Note that only `draft` ran live. `classify` did not run again because its result was already journaled for this `runId`.

## Production note

In this quickstart, you started and resumed the run by running `node` manually. In production, those calls usually come from your own queue worker, webhook handler, job runner, or similar system.

Retry or resume with the same `runId`, and use storage that the resuming process can read. `LocalStorage` is fine for this local example; use shared storage when another machine or serverless invocation may resume the run.

kassette records and replays the workflow. It does not run a server or schedule retries for you. For deployment wiring, see [Wiring the dispatcher](./wiring-dispatcher.md). For a full example, see [Cloudflare Queues + R2](../examples/cloudflare-queue/).

## What just happened

- `classify` ran once. On resume, kassette read its result from the journal instead of running it again.
- The pause survived process exit. `start.ts` stopped as `suspended`; `resume.ts` continued after `ctx.suspend()` with the review result.
- The journal is readable JSONL. You can inspect it with `cat`, `jq`, or store it elsewhere.
- kassette did not start a server. Locally you invoked Node by hand; in production, your queue, webhook, or job runner invokes the workflow.

## Next steps

- Try more examples: [Examples](./examples.md).
- Understand the model: [Why kassette](./why-kassette.md).
- Choose your storage: [Storage backends](./storage-backends.md).
- Wire retries and resume calls: [Wiring the dispatcher](./wiring-dispatcher.md).
- Check API details: [Reference](./reference.md).
