Skip to content
Closed
Changes from all commits
Commits
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
38 changes: 38 additions & 0 deletions docs/sdk/services/workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,44 @@ export const mainJob = createWorkflowJob({

**Important:** On the Tailor runtime, job triggers are executed synchronously. This means `Promise.all([jobA.trigger(), jobB.trigger()])` will not run jobs in parallel.

### Deterministic Execution Requirement

Workflow jobs use a **suspend/resume execution model**. When a job calls `.trigger()`, the runtime suspends the current job, executes the triggered job, and then **re-executes the calling job from the beginning** with cached results from previous triggers.

This means that **job code must be deterministic** — every re-execution must produce the same sequence of `.trigger()` calls with the same arguments in the same order.

Using `.trigger()` inside a loop works correctly, as long as the loop is deterministic:

```typescript
// ✅ OK: deterministic loop — same calls in the same order on every execution
const regions = ["us", "eu", "ap"];
for (const region of regions) {
const result = await fetchData.trigger({ region });
results.push(result);
}
```

```typescript
// ❌ Bad: non-deterministic — argument changes between executions
await processJob.trigger({ timestamp: Date.now() });
```

```typescript
// ❌ Bad: non-deterministic — external data may change between executions
const items = await fetch("https://api.example.com/items").then((r) => r.json());
for (const item of items) {
await processItem.trigger({ id: item.id });
}
```
Comment on lines +135 to +146

@toiroakr toiroakr Apr 9, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought it might be even better if there were examples of how to rewrite the "bad" examples.

// ❌ Bad: non-deterministic — argument changes between executions
await processJob.trigger({ timestamp: Date.now() });

// ✅ OK: call Date.now() in separated job
const timestamp = await timestampJob.trigger();
await processJob.trigger({ timestamp });
// ❌ Bad: non-deterministic — external data may change between executions
const items = await fetch("https://api.example.com/items").then((r) => r.json());
for (const item of items) {
  await processItem.trigger({ id: item.id });
}

// ✅ OK: call fetch("https://api.example.com/items").then((r) => r.json()); in separated job
const items = await fetchItemsJob.trigger();
for (const item of items) {
  await processItem.trigger({ id: item.id });
}


If the runtime detects that a `.trigger()` call at the same position has different arguments than the previous execution, it will throw an **argument hash mismatch error**.

**Guidelines:**

- Do not use non-deterministic values (random numbers, timestamps, external API responses) as `.trigger()` arguments.
- Do not use conditions that may change between executions to decide whether to call `.trigger()`.
- Any data that varies between executions should be fetched **inside the triggered job**, not passed as an argument from the calling job.

## Workflow Definition

Define a workflow using `createWorkflow` and export it as default:
Expand Down
Loading