Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
103 commits
Select commit Hold shift + click to select a range
ad8b7f1
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Jan 27, 2026
e0cb61b
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Jan 28, 2026
051cadc
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Jan 29, 2026
af8ac1f
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Jan 29, 2026
e68e7d2
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Jan 30, 2026
8305e5b
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Jan 30, 2026
f3da688
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Jan 31, 2026
f8ee413
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 1, 2026
589cbd7
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 2, 2026
e979fdf
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 3, 2026
d4baed2
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 6, 2026
53503a4
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 10, 2026
d51e2e4
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 13, 2026
5123088
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 13, 2026
dd1a307
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 13, 2026
00bcfcd
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 14, 2026
f6a157b
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 15, 2026
816f35b
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 16, 2026
2b87dce
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 17, 2026
146de28
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 18, 2026
0ef6455
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 18, 2026
2d8c690
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 18, 2026
3513471
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 20, 2026
0f5d29e
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 20, 2026
c76001e
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 23, 2026
f97184f
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 24, 2026
f71c531
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 25, 2026
530e598
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 25, 2026
ea38511
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 27, 2026
19b1a3f
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 2, 2026
757bfdb
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 2, 2026
5fb4cfb
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 3, 2026
cb5adc3
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 3, 2026
046fdbc
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 3, 2026
2a7884f
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 3, 2026
60ecae6
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 5, 2026
71a6e95
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 6, 2026
25fb139
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 9, 2026
8fe6a36
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 9, 2026
d7cb1a3
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 10, 2026
99b34fe
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 10, 2026
e63bfca
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 11, 2026
810af84
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 11, 2026
67142f9
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 11, 2026
58b47e7
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 12, 2026
4dfac66
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 12, 2026
a423b68
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 13, 2026
b8f1f2e
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 13, 2026
30fe808
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 13, 2026
972c20b
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 13, 2026
0c882fb
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 13, 2026
e8d3775
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 16, 2026
29a316d
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 16, 2026
70b3e67
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 17, 2026
d97b801
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 17, 2026
e50f179
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 18, 2026
f6f1a4e
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 18, 2026
6bc491f
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 18, 2026
bacb19e
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 18, 2026
6a97701
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 20, 2026
d64bb0e
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 20, 2026
04ef7fd
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 22, 2026
9748e93
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 24, 2026
27a5ca6
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 24, 2026
c0fcfbd
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 26, 2026
3c9b9f5
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 26, 2026
b317a73
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 28, 2026
119d147
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 30, 2026
c77eede
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 1, 2026
8a8732e
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 3, 2026
86b1861
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 5, 2026
47af9ca
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 7, 2026
b0f125e
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 10, 2026
f1dbb56
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 11, 2026
deeab3e
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 12, 2026
ea40643
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 13, 2026
e780833
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 13, 2026
a26e834
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 14, 2026
6ebb540
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 14, 2026
dc18bf6
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 14, 2026
70e6f37
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 14, 2026
edb2946
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 15, 2026
d5b3511
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 15, 2026
1497def
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 16, 2026
429d7fa
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 16, 2026
5ea7996
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 17, 2026
8d3b46f
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 22, 2026
b28953f
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 22, 2026
96d2a1e
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 22, 2026
59427f5
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 23, 2026
573f38b
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 28, 2026
d41a682
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 28, 2026
c6909cb
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 29, 2026
2556fb9
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 29, 2026
ab5cd69
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 May 1, 2026
2605428
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 May 5, 2026
a0c6481
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 May 6, 2026
4cd2ba0
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 May 7, 2026
5874a88
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 May 12, 2026
0345928
Merge remote-tracking branch 'origin/main' into main
karthikscale3 May 15, 2026
7d8c1f5
docs(cookbook): add resume-or-start by hook token recipe
karthikscale3 May 15, 2026
afdd9a0
docs(cookbook): clarify stable application ID in resume-or-start
karthikscale3 May 15, 2026
1bd0047
docs(cookbook): tighten resume-or-start mitigations
karthikscale3 May 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/content/docs/v4/cookbook/common-patterns/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"scheduling",
"timeouts",
"idempotency",
"resume-or-start",
"webhooks"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
---
title: Resume or start by hook token
description: Look up an existing workflow run by a deterministic hook token before calling start(), and why this pattern is not fully atomic under concurrency.
type: guide
summary: Use getHookByToken (or world.hooks.getByToken) to attach to an in-flight run keyed by a stable app ID encoded into the hook token; otherwise start a new run. Pair with stronger idempotency when duplicate starts must be impossible.
---

Some HTTP or RPC handlers need **one durable session per stable application identifier**—an ID your app already uses for something in the product domain, such as a **chat thread ID**, **order ID**, **invoice ID**, or **tenant-scoped resource key** (often a primary key, slug, or composite you store or pass in URLs). That value is **not** the Workflow **`runId`**, which the runtime assigns when you call **`start()`**. Instead you **derive a hook token** from the stable ID (for example `` `session:${chatThreadId}` `` for a chat) so every request about the same real-world entity computes the **same** token.

If a run is already active and has registered a hook with that token, you can **look up the hook**, reuse `hook.runId`, and avoid starting a second workflow. If nothing is registered yet, you **`start()`** a new run.

## When to use this

- **Session handoff** — A client or edge handler hits your API without a `runId`, but you already know a **stable application ID** (path param, session subject, database row key, etc.) and can derive the hook token from it.
- **Reconnect before resume** — You want `getRun(runId)` (streams, `returnValue`, status) without persisting `runId` in your own store, as long as the workflow registers a hook with that token early in the run.
- **Custom worlds / admin tools** — You already use [`world.hooks.getByToken()`](/docs/api-reference/workflow-api/world/storage#look-up-hook-by-token) against storage; the app-level pattern is the same.

## Pattern

1. Build a **deterministic hook token** from your **stable application ID** plus a fixed prefix or namespace so different features do not collide (for example `` `session:${chatThreadId}` ``).
2. Call **`getHookByToken(token)`** from `workflow/api` (or `world.hooks.getByToken(token)` if you hold a `World` instance).
3. If a hook exists, use **`hook.runId`** with [`getRun()`](/docs/api-reference/workflow-api/get-run) — for example await **`run.returnValue`** when you need the workflow outcome.
4. If no hook is found (typically `404` / not found), call **[`start()`](/docs/api-reference/workflow-api/start)** with the same arguments your workflow would use to create that hook on first execution.

```typescript lineNumbers
import { start, getRun, getHookByToken } from "workflow/api";
import { sessionWorkflow } from "./workflows/session";

function sessionToken(chatThreadId: string) {
return `session:${chatThreadId}`;
}

/** `chatThreadId` = your app's stable ID for the conversation (not the Workflow runId). */
export async function getOrStartSession(chatThreadId: string) {
const token = sessionToken(chatThreadId);

const existing = await getHookByToken(token).catch(() => null); // [!code highlight]

if (existing) {
const run = getRun(existing.runId); // [!code highlight]
return run.returnValue;
}

const run = await start(sessionWorkflow, [chatThreadId]); // [!code highlight]
return run.returnValue;
}
```

Your workflow should create the hook with the **same token shape** early (before long-running work), so the lookup succeeds once the run is registered:

```typescript lineNumbers
import { defineHook } from "workflow";

export const sessionHook = defineHook<void>();

export async function sessionWorkflow(chatThreadId: string) {
"use workflow";

const token = `session:${chatThreadId}`;
const hook = sessionHook.create({ token }); // [!code highlight]

// ... durable session logic, await hook, etc.
}
```

## Concurrency and atomicity

<Callout type="warn">
This pattern is **not atomic**. Two concurrent requests can both observe “no hook” **before** the first run has written its `hook_created` event, so both can call **`start()`** and you can get **two runs** for the same stable application ID (the same derived hook token).
</Callout>

Mitigations:

- **Accept rare duplicates** and detect them downstream (second run fails on `HookConflictError`, or you branch on run state).
- **Add an outside-workflow coordination layer** — for example a **row keyed by your stable application ID** (the same value you embed in the hook token) with a **unique constraint** that stores the authoritative **`runId`**, using **insert-if-absent**, **`INSERT … ON CONFLICT`**, or a **transaction with row-level locking** so only one concurrent handler reaches **`start()`** for that ID. A bare unique constraint on your domain table primary key does not by itself dedupe workflow runs; you need a **dedicated mapping or lease** tied to that ID. **`start()`** does not expose an idempotency-key option today (only `world`, `specVersion`, and `deploymentId` in [`StartOptions`](/docs/api-reference/workflow-api/start)).
- **Serialize creates** for the same stable ID (queue, advisory lock, or other mutual exclusion) if you must not spin up a second run even briefly.

For replay-safe **step** side effects inside a workflow, still follow [Idempotency](/cookbook/common-patterns/idempotency) (`getStepMetadata().stepId`, external idempotency keys).

## Key APIs

- [`getHookByToken()`](/docs/api-reference/workflow-api/get-hook-by-token) — resolve `runId` (and metadata) from a hook token in app code.
- [`world.hooks.getByToken()`](/docs/api-reference/workflow-api/world/storage#look-up-hook-by-token) — same lookup against a `World` storage interface.
- [`start()`](/docs/api-reference/workflow-api/start) — enqueue a new run when no hook exists yet.
- [`getRun()`](/docs/api-reference/workflow-api/get-run) — attach to an existing run by `runId` (`returnValue`, streams, status).

## Related

- [Distributed Abort Controller](/cookbook/advanced/distributed-abort-controller) — full example of reconnect-or-`start()` for a cross-process abort controller.
- [Idempotency](/cookbook/common-patterns/idempotency) — keys for external APIs and duplicate-safe side effects inside steps.
1 change: 1 addition & 0 deletions docs/content/docs/v4/cookbook/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ A curated collection of workflow patterns with clean, copy-paste code examples f
- [**Scheduling**](/cookbook/common-patterns/scheduling) — Use durable sleep to schedule actions minutes, hours, or weeks ahead
- [**Timeouts**](/cookbook/common-patterns/timeouts) — Add deadlines to slow steps, hooks, and webhooks by racing them against a durable sleep
- [**Idempotency**](/cookbook/common-patterns/idempotency) — Ensure side effects happen exactly once, even when steps retry
- [**Resume or start by hook token**](/cookbook/common-patterns/resume-or-start) — Encode a stable application ID in the hook token, look up an existing run with `getHookByToken` before `start()`, and understand the concurrency limits
- [**Webhooks**](/cookbook/common-patterns/webhooks) — Receive HTTP callbacks from external services and process them durably

## Integrations
Expand Down
1 change: 1 addition & 0 deletions docs/content/docs/v5/cookbook/common-patterns/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"scheduling",
"timeouts",
"idempotency",
"resume-or-start",
"webhooks"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
---
title: Resume or start by hook token
description: Look up an existing workflow run by a deterministic hook token before calling start(), and why this pattern is not fully atomic under concurrency.
type: guide
summary: Use getHookByToken (or world.hooks.getByToken) to attach to an in-flight run keyed by a stable app ID encoded into the hook token; otherwise start a new run. Pair with stronger idempotency when duplicate starts must be impossible.
---

Some HTTP or RPC handlers need **one durable session per stable application identifier**—an ID your app already uses for something in the product domain, such as a **chat thread ID**, **order ID**, **invoice ID**, or **tenant-scoped resource key** (often a primary key, slug, or composite you store or pass in URLs). That value is **not** the Workflow **`runId`**, which the runtime assigns when you call **`start()`**. Instead you **derive a hook token** from the stable ID (for example `` `session:${chatThreadId}` `` for a chat) so every request about the same real-world entity computes the **same** token.

If a run is already active and has registered a hook with that token, you can **look up the hook**, reuse `hook.runId`, and avoid starting a second workflow. If nothing is registered yet, you **`start()`** a new run.

## When to use this

- **Session handoff** — A client or edge handler hits your API without a `runId`, but you already know a **stable application ID** (path param, session subject, database row key, etc.) and can derive the hook token from it.
- **Reconnect before resume** — You want `getRun(runId)` (streams, `returnValue`, status) without persisting `runId` in your own store, as long as the workflow registers a hook with that token early in the run.
- **Custom worlds / admin tools** — You already use [`world.hooks.getByToken()`](/docs/api-reference/workflow-api/world/storage#look-up-hook-by-token) against storage; the app-level pattern is the same.

## Pattern

1. Build a **deterministic hook token** from your **stable application ID** plus a fixed prefix or namespace so different features do not collide (for example `` `session:${chatThreadId}` ``).
2. Call **`getHookByToken(token)`** from `workflow/api` (or `world.hooks.getByToken(token)` if you hold a `World` instance).
3. If a hook exists, use **`hook.runId`** with [`getRun()`](/docs/api-reference/workflow-api/get-run) — for example await **`run.returnValue`** when you need the workflow outcome.
4. If no hook is found (typically `404` / not found), call **[`start()`](/docs/api-reference/workflow-api/start)** with the same arguments your workflow would use to create that hook on first execution.

```typescript lineNumbers
import { start, getRun, getHookByToken } from "workflow/api";
import { sessionWorkflow } from "./workflows/session";

function sessionToken(chatThreadId: string) {
return `session:${chatThreadId}`;
}

/** `chatThreadId` = your app's stable ID for the conversation (not the Workflow runId). */
export async function getOrStartSession(chatThreadId: string) {
const token = sessionToken(chatThreadId);

const existing = await getHookByToken(token).catch(() => null); // [!code highlight]

if (existing) {
const run = getRun(existing.runId); // [!code highlight]
return run.returnValue;
}

const run = await start(sessionWorkflow, [chatThreadId]); // [!code highlight]
return run.returnValue;
}
```

Your workflow should create the hook with the **same token shape** early (before long-running work), so the lookup succeeds once the run is registered:

```typescript lineNumbers
import { defineHook } from "workflow";

export const sessionHook = defineHook<void>();

export async function sessionWorkflow(chatThreadId: string) {
"use workflow";

const token = `session:${chatThreadId}`;
const hook = sessionHook.create({ token }); // [!code highlight]

// ... durable session logic, await hook, etc.
}
```

## Concurrency and atomicity

<Callout type="warn">
This pattern is **not atomic**. Two concurrent requests can both observe “no hook” **before** the first run has written its `hook_created` event, so both can call **`start()`** and you can get **two runs** for the same stable application ID (the same derived hook token).
</Callout>

Mitigations:

- **Accept rare duplicates** and detect them downstream (second run fails on `HookConflictError`, or you branch on run state).
- **Add an outside-workflow coordination layer** — for example a **row keyed by your stable application ID** (the same value you embed in the hook token) with a **unique constraint** that stores the authoritative **`runId`**, using **insert-if-absent**, **`INSERT … ON CONFLICT`**, or a **transaction with row-level locking** so only one concurrent handler reaches **`start()`** for that ID. A bare unique constraint on your domain table primary key does not by itself dedupe workflow runs; you need a **dedicated mapping or lease** tied to that ID. **`start()`** does not expose an idempotency-key option today (only `world`, `specVersion`, and `deploymentId` in [`StartOptions`](/docs/api-reference/workflow-api/start)).
- **Serialize creates** for the same stable ID (queue, advisory lock, or other mutual exclusion) if you must not spin up a second run even briefly.

For replay-safe **step** side effects inside a workflow, still follow [Idempotency](/cookbook/common-patterns/idempotency) (`getStepMetadata().stepId`, external idempotency keys).

## Key APIs

- [`getHookByToken()`](/docs/api-reference/workflow-api/get-hook-by-token) — resolve `runId` (and metadata) from a hook token in app code.
- [`world.hooks.getByToken()`](/docs/api-reference/workflow-api/world/storage#look-up-hook-by-token) — same lookup against a `World` storage interface.
- [`start()`](/docs/api-reference/workflow-api/start) — enqueue a new run when no hook exists yet.
- [`getRun()`](/docs/api-reference/workflow-api/get-run) — attach to an existing run by `runId` (`returnValue`, streams, status).

## Related

- [Idempotency](/cookbook/common-patterns/idempotency) — keys for external APIs and duplicate-safe side effects inside steps.
1 change: 1 addition & 0 deletions docs/content/docs/v5/cookbook/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ A curated collection of workflow patterns with clean, copy-paste code examples f
- [**Scheduling**](/cookbook/common-patterns/scheduling) — Use durable sleep to schedule actions minutes, hours, or weeks ahead
- [**Timeouts**](/cookbook/common-patterns/timeouts) — Add deadlines to slow steps, hooks, and webhooks by racing them against a durable sleep
- [**Idempotency**](/cookbook/common-patterns/idempotency) — Ensure side effects happen exactly once, even when steps retry
- [**Resume or start by hook token**](/cookbook/common-patterns/resume-or-start) — Encode a stable application ID in the hook token, look up an existing run with `getHookByToken` before `start()`, and understand the concurrency limits
- [**Webhooks**](/cookbook/common-patterns/webhooks) — Receive HTTP callbacks from external services and process them durably

## Integrations
Expand Down
8 changes: 8 additions & 0 deletions docs/lib/cookbook-tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const slugToCategory: Record<string, string> = {
scheduling: 'common-patterns',
timeouts: 'common-patterns',
idempotency: 'common-patterns',
'resume-or-start': 'common-patterns',
webhooks: 'common-patterns',

// Agent Patterns
Expand Down Expand Up @@ -119,6 +120,13 @@ export const recipes: Record<string, Recipe> = {
'Ensure external side effects happen exactly once, even when steps are retried or workflows are replayed.',
category: 'common-patterns',
},
'resume-or-start': {
slug: 'resume-or-start',
title: 'Resume or start by hook token',
description:
'Look up an existing run with getHookByToken (or world.hooks.getByToken) before start(); encode a stable application ID (not runId) in the hook token. Pair with stronger idempotency when duplicate starts must be impossible.',
category: 'common-patterns',
},
webhooks: {
slug: 'webhooks',
title: 'Webhooks & External Callbacks',
Expand Down
4 changes: 3 additions & 1 deletion packages/world-local/src/queue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,9 @@ describe('queue timeout re-enqueue', () => {
});

it('logs actionable guidance for detached ArrayBuffer proxy failures', async () => {
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {});
const consoleError = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
const fetchError = new TypeError('fetch failed');
(fetchError as TypeError & { cause?: unknown }).cause = new TypeError(
'Cannot perform ArrayBuffer.prototype.slice on a detached ArrayBuffer'
Expand Down
Loading