Merged
Show file tree
Hide file tree
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
6 changes: 6 additions & 0 deletions .changeset/timeout-functions-error.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
---
"@workflow/errors": patch
"@workflow/core": patch
---

Override setTimeout, setInterval, and related functions in workflow VM context to throw helpful errors suggesting to use `sleep` instead
3 changes: 3 additions & 0 deletions docs/content/docs/errors/index.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,6 +17,9 @@ Fix common mistakes when creating and executing workflows in the **Workflow DevK
<Card href="/docs/errors/start-invalid-workflow-function" title="start-invalid-workflow-function">
Learn how to start an invalid workflow function.
</Card>
<Card href="/docs/errors/timeout-in-workflow" title="timeout-in-workflow">
Learn how to handle timing delays in workflow functions.
</Card>
<Card href="/docs/errors/webhook-invalid-respond-with-value" title="webhook-invalid-respond-with-value">
Learn how to use the correct `respondWith` values for webhooks.
</Card>
Expand Down
120 changes: 120 additions & 0 deletions docs/content/docs/errors/timeout-in-workflow.mdx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
---
title: timeout-in-workflow
---

This error occurs when you try to use `setTimeout()`, `setInterval()`, or related timing functions directly inside a workflow function.

## Error Message

```
Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions. Use the "sleep" function from "workflow" for time-based delays.
```

## Why This Happens

Workflow functions run in a sandboxed environment where timing functions like `setTimeout()` and `setInterval()` are not available. These functions rely on asynchronous scheduling that would break the **deterministic replay** guarantees that workflows depend on.

When a workflow suspends and later resumes, it replays from the event log. If timing functions were allowed, the replay would produce different results than the original execution.

## Quick Fix

Use the `sleep` function from the `workflow` package for time-based delays. Unlike `setTimeout()`, `sleep` is tracked in the event log and replays correctly.

**Before:**

```typescript lineNumbers title="workflows/delayed.ts"
export async function delayedWorkflow() {
"use workflow";

// Error - setTimeout is not available in workflow functions
await new Promise(resolve => setTimeout(resolve, 5000)); // [!code highlight]

return 'done';
}
```

**After:**

```typescript lineNumbers title="workflows/delayed.ts"
import { sleep } from 'workflow'; // [!code highlight]

export async function delayedWorkflow() {
"use workflow";

// sleep is tracked in the event log and replays correctly
await sleep('5s'); // [!code highlight]

return 'done';
}
```

## Unavailable Functions

These timing functions cannot be used in workflow functions:

- `setTimeout()`
- `setInterval()`
- `setImmediate()`
- `clearTimeout()`
- `clearInterval()`
- `clearImmediate()`
Comment thread
TooTallNate marked this conversation as resolved.

## Common Scenarios

### Polling with Delays

If you need to poll an external service with delays between requests:

```typescript lineNumbers title="workflows/polling.ts"
import { sleep } from 'workflow';

export async function pollingWorkflow() {
"use workflow";

let status = 'pending';

while (status === 'pending') {
status = await checkStatus(); // step function
if (status === 'pending') {
await sleep('10s'); // [!code highlight]
}
}

return status;
}

async function checkStatus() {
"use step";
const response = await fetch('https://api.example.com/status');
const data = await response.json();
return data.status;
}
```

### Scheduled Delays

For workflows that need to wait for a specific duration:

```typescript lineNumbers title="workflows/reminder.ts"
import { sleep } from 'workflow';

export async function reminderWorkflow(message: string) {
"use workflow";

// Wait 24 hours before sending reminder
await sleep('24h'); // [!code highlight]

await sendReminder(message);

return 'reminder sent';
}

async function sendReminder(message: string) {
"use step";
// Send reminder logic
}
```

<Callout type="info">
The `sleep` function accepts duration strings like `'5s'`, `'10m'`, `'1h'`, `'24h'`, or milliseconds as a number. See the [sleep API reference](/docs/api-reference/workflow/sleep) for more details.
</Callout>
213 changes: 213 additions & 0 deletions packages/core/src/workflow.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -947,6 +947,219 @@ describe('runWorkflow', () => {
});
});

describe('timeout functions', () => {
it('should throw an error when calling setTimeout', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
setTimeout(() => {}, 1000);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should throw an error when calling setInterval', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
setInterval(() => {}, 1000);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should throw an error when calling clearTimeout', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
clearTimeout(123);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should throw an error when calling clearInterval', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
clearInterval(123);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should throw an error when calling setImmediate', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
setImmediate(() => {});
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should throw an error when calling clearImmediate', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
clearImmediate(123);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should include documentation link in error message', async () => {
let error: Error | undefined;
try {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await runWorkflow(
`async function workflow() {
setTimeout(() => {}, 1000);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
);
} catch (err) {
error = err as Error;
}
assert(error);
expect(error.message).toContain(
'https://useworkflow.dev/err/timeout-in-workflow'
);
expect(error.message).toContain(
'Use the "sleep" function from "workflow"'
);
});
});

describe('hook', () => {
it('should throw `WorkflowSuspension` when a hook is awaiting without a "hook_received" event', async () => {
let error: Error | undefined;
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
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
6 changes: 6 additions & 0 deletions .changeset/timeout-functions-error.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
---
"@workflow/errors": patch
"@workflow/core": patch
---

Override setTimeout, setInterval, and related functions in workflow VM context to throw helpful errors suggesting to use `sleep` instead
3 changes: 3 additions & 0 deletions docs/content/docs/errors/index.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,6 +17,9 @@ Fix common mistakes when creating and executing workflows in the **Workflow DevK
<Card href="/docs/errors/start-invalid-workflow-function" title="start-invalid-workflow-function">
Learn how to start an invalid workflow function.
</Card>
<Card href="/docs/errors/timeout-in-workflow" title="timeout-in-workflow">
Learn how to handle timing delays in workflow functions.
</Card>
<Card href="/docs/errors/webhook-invalid-respond-with-value" title="webhook-invalid-respond-with-value">
Learn how to use the correct `respondWith` values for webhooks.
</Card>
Expand Down
120 changes: 120 additions & 0 deletions docs/content/docs/errors/timeout-in-workflow.mdx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
---
title: timeout-in-workflow
---

This error occurs when you try to use `setTimeout()`, `setInterval()`, or related timing functions directly inside a workflow function.

## Error Message

```
Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions. Use the "sleep" function from "workflow" for time-based delays.
```

## Why This Happens

Workflow functions run in a sandboxed environment where timing functions like `setTimeout()` and `setInterval()` are not available. These functions rely on asynchronous scheduling that would break the **deterministic replay** guarantees that workflows depend on.

When a workflow suspends and later resumes, it replays from the event log. If timing functions were allowed, the replay would produce different results than the original execution.

## Quick Fix

Use the `sleep` function from the `workflow` package for time-based delays. Unlike `setTimeout()`, `sleep` is tracked in the event log and replays correctly.

**Before:**

```typescript lineNumbers title="workflows/delayed.ts"
export async function delayedWorkflow() {
"use workflow";

// Error - setTimeout is not available in workflow functions
await new Promise(resolve => setTimeout(resolve, 5000)); // [!code highlight]

return 'done';
}
```

**After:**

```typescript lineNumbers title="workflows/delayed.ts"
import { sleep } from 'workflow'; // [!code highlight]

export async function delayedWorkflow() {
"use workflow";

// sleep is tracked in the event log and replays correctly
await sleep('5s'); // [!code highlight]

return 'done';
}
```

## Unavailable Functions

These timing functions cannot be used in workflow functions:

- `setTimeout()`
- `setInterval()`
- `setImmediate()`
- `clearTimeout()`
- `clearInterval()`
- `clearImmediate()`
Comment thread
TooTallNate marked this conversation as resolved.

## Common Scenarios

### Polling with Delays

If you need to poll an external service with delays between requests:

```typescript lineNumbers title="workflows/polling.ts"
import { sleep } from 'workflow';

export async function pollingWorkflow() {
"use workflow";

let status = 'pending';

while (status === 'pending') {
status = await checkStatus(); // step function
if (status === 'pending') {
await sleep('10s'); // [!code highlight]
}
}

return status;
}

async function checkStatus() {
"use step";
const response = await fetch('https://api.example.com/status');
const data = await response.json();
return data.status;
}
```

### Scheduled Delays

For workflows that need to wait for a specific duration:

```typescript lineNumbers title="workflows/reminder.ts"
import { sleep } from 'workflow';

export async function reminderWorkflow(message: string) {
"use workflow";

// Wait 24 hours before sending reminder
await sleep('24h'); // [!code highlight]

await sendReminder(message);

return 'reminder sent';
}

async function sendReminder(message: string) {
"use step";
// Send reminder logic
}
```

<Callout type="info">
The `sleep` function accepts duration strings like `'5s'`, `'10m'`, `'1h'`, `'24h'`, or milliseconds as a number. See the [sleep API reference](/docs/api-reference/workflow/sleep) for more details.
</Callout>
213 changes: 213 additions & 0 deletions packages/core/src/workflow.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -947,6 +947,219 @@ describe('runWorkflow', () => {
});
});

describe('timeout functions', () => {
it('should throw an error when calling setTimeout', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
setTimeout(() => {}, 1000);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should throw an error when calling setInterval', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
setInterval(() => {}, 1000);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should throw an error when calling clearTimeout', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
clearTimeout(123);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should throw an error when calling clearInterval', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
clearInterval(123);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should throw an error when calling setImmediate', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
setImmediate(() => {});
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should throw an error when calling clearImmediate', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
clearImmediate(123);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should include documentation link in error message', async () => {
let error: Error | undefined;
try {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await runWorkflow(
`async function workflow() {
setTimeout(() => {}, 1000);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
);
} catch (err) {
error = err as Error;
}
assert(error);
expect(error.message).toContain(
'https://useworkflow.dev/err/timeout-in-workflow'
);
expect(error.message).toContain(
'Use the "sleep" function from "workflow"'
);
});
});

describe('hook', () => {
it('should throw `WorkflowSuspension` when a hook is awaiting without a "hook_received" event', async () => {
let error: Error | undefined;
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
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
6 changes: 6 additions & 0 deletions .changeset/timeout-functions-error.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
---
"@workflow/errors": patch
"@workflow/core": patch
---

Override setTimeout, setInterval, and related functions in workflow VM context to throw helpful errors suggesting to use `sleep` instead
3 changes: 3 additions & 0 deletions docs/content/docs/errors/index.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,6 +17,9 @@ Fix common mistakes when creating and executing workflows in the **Workflow DevK
<Card href="/docs/errors/start-invalid-workflow-function" title="start-invalid-workflow-function">
Learn how to start an invalid workflow function.
</Card>
<Card href="/docs/errors/timeout-in-workflow" title="timeout-in-workflow">
Learn how to handle timing delays in workflow functions.
</Card>
<Card href="/docs/errors/webhook-invalid-respond-with-value" title="webhook-invalid-respond-with-value">
Learn how to use the correct `respondWith` values for webhooks.
</Card>
Expand Down
120 changes: 120 additions & 0 deletions docs/content/docs/errors/timeout-in-workflow.mdx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
---
title: timeout-in-workflow
---

This error occurs when you try to use `setTimeout()`, `setInterval()`, or related timing functions directly inside a workflow function.

## Error Message

```
Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions. Use the "sleep" function from "workflow" for time-based delays.
```

## Why This Happens

Workflow functions run in a sandboxed environment where timing functions like `setTimeout()` and `setInterval()` are not available. These functions rely on asynchronous scheduling that would break the **deterministic replay** guarantees that workflows depend on.

When a workflow suspends and later resumes, it replays from the event log. If timing functions were allowed, the replay would produce different results than the original execution.

## Quick Fix

Use the `sleep` function from the `workflow` package for time-based delays. Unlike `setTimeout()`, `sleep` is tracked in the event log and replays correctly.

**Before:**

```typescript lineNumbers title="workflows/delayed.ts"
export async function delayedWorkflow() {
"use workflow";

// Error - setTimeout is not available in workflow functions
await new Promise(resolve => setTimeout(resolve, 5000)); // [!code highlight]

return 'done';
}
```

**After:**

```typescript lineNumbers title="workflows/delayed.ts"
import { sleep } from 'workflow'; // [!code highlight]

export async function delayedWorkflow() {
"use workflow";

// sleep is tracked in the event log and replays correctly
await sleep('5s'); // [!code highlight]

return 'done';
}
```

## Unavailable Functions

These timing functions cannot be used in workflow functions:

- `setTimeout()`
- `setInterval()`
- `setImmediate()`
- `clearTimeout()`
- `clearInterval()`
- `clearImmediate()`
Comment thread
TooTallNate marked this conversation as resolved.

## Common Scenarios

### Polling with Delays

If you need to poll an external service with delays between requests:

```typescript lineNumbers title="workflows/polling.ts"
import { sleep } from 'workflow';

export async function pollingWorkflow() {
"use workflow";

let status = 'pending';

while (status === 'pending') {
status = await checkStatus(); // step function
if (status === 'pending') {
await sleep('10s'); // [!code highlight]
}
}

return status;
}

async function checkStatus() {
"use step";
const response = await fetch('https://api.example.com/status');
const data = await response.json();
return data.status;
}
```

### Scheduled Delays

For workflows that need to wait for a specific duration:

```typescript lineNumbers title="workflows/reminder.ts"
import { sleep } from 'workflow';

export async function reminderWorkflow(message: string) {
"use workflow";

// Wait 24 hours before sending reminder
await sleep('24h'); // [!code highlight]

await sendReminder(message);

return 'reminder sent';
}

async function sendReminder(message: string) {
"use step";
// Send reminder logic
}
```

<Callout type="info">
The `sleep` function accepts duration strings like `'5s'`, `'10m'`, `'1h'`, `'24h'`, or milliseconds as a number. See the [sleep API reference](/docs/api-reference/workflow/sleep) for more details.
</Callout>
213 changes: 213 additions & 0 deletions packages/core/src/workflow.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -947,6 +947,219 @@ describe('runWorkflow', () => {
});
});

describe('timeout functions', () => {
it('should throw an error when calling setTimeout', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
setTimeout(() => {}, 1000);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should throw an error when calling setInterval', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
setInterval(() => {}, 1000);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should throw an error when calling clearTimeout', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
clearTimeout(123);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should throw an error when calling clearInterval', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
clearInterval(123);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should throw an error when calling setImmediate', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
setImmediate(() => {});
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should throw an error when calling clearImmediate', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
clearImmediate(123);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should include documentation link in error message', async () => {
let error: Error | undefined;
try {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await runWorkflow(
`async function workflow() {
setTimeout(() => {}, 1000);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
);
} catch (err) {
error = err as Error;
}
assert(error);
expect(error.message).toContain(
'https://useworkflow.dev/err/timeout-in-workflow'
);
expect(error.message).toContain(
'Use the "sleep" function from "workflow"'
);
});
});

describe('hook', () => {
it('should throw `WorkflowSuspension` when a hook is awaiting without a "hook_received" event', async () => {
let error: Error | undefined;
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
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
6 changes: 6 additions & 0 deletions .changeset/timeout-functions-error.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
---
"@workflow/errors": patch
"@workflow/core": patch
---

Override setTimeout, setInterval, and related functions in workflow VM context to throw helpful errors suggesting to use `sleep` instead
3 changes: 3 additions & 0 deletions docs/content/docs/errors/index.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,6 +17,9 @@ Fix common mistakes when creating and executing workflows in the **Workflow DevK
<Card href="/docs/errors/start-invalid-workflow-function" title="start-invalid-workflow-function">
Learn how to start an invalid workflow function.
</Card>
<Card href="/docs/errors/timeout-in-workflow" title="timeout-in-workflow">
Learn how to handle timing delays in workflow functions.
</Card>
<Card href="/docs/errors/webhook-invalid-respond-with-value" title="webhook-invalid-respond-with-value">
Learn how to use the correct `respondWith` values for webhooks.
</Card>
Expand Down
120 changes: 120 additions & 0 deletions docs/content/docs/errors/timeout-in-workflow.mdx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
---
title: timeout-in-workflow
---

This error occurs when you try to use `setTimeout()`, `setInterval()`, or related timing functions directly inside a workflow function.

## Error Message

```
Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions. Use the "sleep" function from "workflow" for time-based delays.
```

## Why This Happens

Workflow functions run in a sandboxed environment where timing functions like `setTimeout()` and `setInterval()` are not available. These functions rely on asynchronous scheduling that would break the **deterministic replay** guarantees that workflows depend on.

When a workflow suspends and later resumes, it replays from the event log. If timing functions were allowed, the replay would produce different results than the original execution.

## Quick Fix

Use the `sleep` function from the `workflow` package for time-based delays. Unlike `setTimeout()`, `sleep` is tracked in the event log and replays correctly.

**Before:**

```typescript lineNumbers title="workflows/delayed.ts"
export async function delayedWorkflow() {
"use workflow";

// Error - setTimeout is not available in workflow functions
await new Promise(resolve => setTimeout(resolve, 5000)); // [!code highlight]

return 'done';
}
```

**After:**

```typescript lineNumbers title="workflows/delayed.ts"
import { sleep } from 'workflow'; // [!code highlight]

export async function delayedWorkflow() {
"use workflow";

// sleep is tracked in the event log and replays correctly
await sleep('5s'); // [!code highlight]

return 'done';
}
```

## Unavailable Functions

These timing functions cannot be used in workflow functions:

- `setTimeout()`
- `setInterval()`
- `setImmediate()`
- `clearTimeout()`
- `clearInterval()`
- `clearImmediate()`
Comment thread
TooTallNate marked this conversation as resolved.

## Common Scenarios

### Polling with Delays

If you need to poll an external service with delays between requests:

```typescript lineNumbers title="workflows/polling.ts"
import { sleep } from 'workflow';

export async function pollingWorkflow() {
"use workflow";

let status = 'pending';

while (status === 'pending') {
status = await checkStatus(); // step function
if (status === 'pending') {
await sleep('10s'); // [!code highlight]
}
}

return status;
}

async function checkStatus() {
"use step";
const response = await fetch('https://api.example.com/status');
const data = await response.json();
return data.status;
}
```

### Scheduled Delays

For workflows that need to wait for a specific duration:

```typescript lineNumbers title="workflows/reminder.ts"
import { sleep } from 'workflow';

export async function reminderWorkflow(message: string) {
"use workflow";

// Wait 24 hours before sending reminder
await sleep('24h'); // [!code highlight]

await sendReminder(message);

return 'reminder sent';
}

async function sendReminder(message: string) {
"use step";
// Send reminder logic
}
```

<Callout type="info">
The `sleep` function accepts duration strings like `'5s'`, `'10m'`, `'1h'`, `'24h'`, or milliseconds as a number. See the [sleep API reference](/docs/api-reference/workflow/sleep) for more details.
</Callout>
213 changes: 213 additions & 0 deletions packages/core/src/workflow.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -947,6 +947,219 @@ describe('runWorkflow', () => {
});
});

describe('timeout functions', () => {
it('should throw an error when calling setTimeout', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
setTimeout(() => {}, 1000);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should throw an error when calling setInterval', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
setInterval(() => {}, 1000);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should throw an error when calling clearTimeout', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
clearTimeout(123);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should throw an error when calling clearInterval', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
clearInterval(123);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should throw an error when calling setImmediate', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
setImmediate(() => {});
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should throw an error when calling clearImmediate', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
clearImmediate(123);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should include documentation link in error message', async () => {
let error: Error | undefined;
try {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await runWorkflow(
`async function workflow() {
setTimeout(() => {}, 1000);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
);
} catch (err) {
error = err as Error;
}
assert(error);
expect(error.message).toContain(
'https://useworkflow.dev/err/timeout-in-workflow'
);
expect(error.message).toContain(
'Use the "sleep" function from "workflow"'
);
});
});

describe('hook', () => {
it('should throw `WorkflowSuspension` when a hook is awaiting without a "hook_received" event', async () => {
let error: Error | undefined;
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
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
6 changes: 6 additions & 0 deletions .changeset/timeout-functions-error.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
---
"@workflow/errors": patch
"@workflow/core": patch
---

Override setTimeout, setInterval, and related functions in workflow VM context to throw helpful errors suggesting to use `sleep` instead
3 changes: 3 additions & 0 deletions docs/content/docs/errors/index.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,6 +17,9 @@ Fix common mistakes when creating and executing workflows in the **Workflow DevK
<Card href="/docs/errors/start-invalid-workflow-function" title="start-invalid-workflow-function">
Learn how to start an invalid workflow function.
</Card>
<Card href="/docs/errors/timeout-in-workflow" title="timeout-in-workflow">
Learn how to handle timing delays in workflow functions.
</Card>
<Card href="/docs/errors/webhook-invalid-respond-with-value" title="webhook-invalid-respond-with-value">
Learn how to use the correct `respondWith` values for webhooks.
</Card>
Expand Down
120 changes: 120 additions & 0 deletions docs/content/docs/errors/timeout-in-workflow.mdx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
---
title: timeout-in-workflow
---

This error occurs when you try to use `setTimeout()`, `setInterval()`, or related timing functions directly inside a workflow function.

## Error Message

```
Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions. Use the "sleep" function from "workflow" for time-based delays.
```

## Why This Happens

Workflow functions run in a sandboxed environment where timing functions like `setTimeout()` and `setInterval()` are not available. These functions rely on asynchronous scheduling that would break the **deterministic replay** guarantees that workflows depend on.

When a workflow suspends and later resumes, it replays from the event log. If timing functions were allowed, the replay would produce different results than the original execution.

## Quick Fix

Use the `sleep` function from the `workflow` package for time-based delays. Unlike `setTimeout()`, `sleep` is tracked in the event log and replays correctly.

**Before:**

```typescript lineNumbers title="workflows/delayed.ts"
export async function delayedWorkflow() {
"use workflow";

// Error - setTimeout is not available in workflow functions
await new Promise(resolve => setTimeout(resolve, 5000)); // [!code highlight]

return 'done';
}
```

**After:**

```typescript lineNumbers title="workflows/delayed.ts"
import { sleep } from 'workflow'; // [!code highlight]

export async function delayedWorkflow() {
"use workflow";

// sleep is tracked in the event log and replays correctly
await sleep('5s'); // [!code highlight]

return 'done';
}
```

## Unavailable Functions

These timing functions cannot be used in workflow functions:

- `setTimeout()`
- `setInterval()`
- `setImmediate()`
- `clearTimeout()`
- `clearInterval()`
- `clearImmediate()`
Comment thread
TooTallNate marked this conversation as resolved.

## Common Scenarios

### Polling with Delays

If you need to poll an external service with delays between requests:

```typescript lineNumbers title="workflows/polling.ts"
import { sleep } from 'workflow';

export async function pollingWorkflow() {
"use workflow";

let status = 'pending';

while (status === 'pending') {
status = await checkStatus(); // step function
if (status === 'pending') {
await sleep('10s'); // [!code highlight]
}
}

return status;
}

async function checkStatus() {
"use step";
const response = await fetch('https://api.example.com/status');
const data = await response.json();
return data.status;
}
```

### Scheduled Delays

For workflows that need to wait for a specific duration:

```typescript lineNumbers title="workflows/reminder.ts"
import { sleep } from 'workflow';

export async function reminderWorkflow(message: string) {
"use workflow";

// Wait 24 hours before sending reminder
await sleep('24h'); // [!code highlight]

await sendReminder(message);

return 'reminder sent';
}

async function sendReminder(message: string) {
"use step";
// Send reminder logic
}
```

<Callout type="info">
The `sleep` function accepts duration strings like `'5s'`, `'10m'`, `'1h'`, `'24h'`, or milliseconds as a number. See the [sleep API reference](/docs/api-reference/workflow/sleep) for more details.
</Callout>
213 changes: 213 additions & 0 deletions packages/core/src/workflow.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -947,6 +947,219 @@ describe('runWorkflow', () => {
});
});

describe('timeout functions', () => {
it('should throw an error when calling setTimeout', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
setTimeout(() => {}, 1000);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should throw an error when calling setInterval', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
setInterval(() => {}, 1000);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should throw an error when calling clearTimeout', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
clearTimeout(123);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should throw an error when calling clearInterval', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
clearInterval(123);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should throw an error when calling setImmediate', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
setImmediate(() => {});
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should throw an error when calling clearImmediate', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
clearImmediate(123);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should include documentation link in error message', async () => {
let error: Error | undefined;
try {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await runWorkflow(
`async function workflow() {
setTimeout(() => {}, 1000);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
);
} catch (err) {
error = err as Error;
}
assert(error);
expect(error.message).toContain(
'https://useworkflow.dev/err/timeout-in-workflow'
);
expect(error.message).toContain(
'Use the "sleep" function from "workflow"'
);
});
});

describe('hook', () => {
it('should throw `WorkflowSuspension` when a hook is awaiting without a "hook_received" event', async () => {
let error: Error | undefined;
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
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
6 changes: 6 additions & 0 deletions .changeset/timeout-functions-error.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
---
"@workflow/errors": patch
"@workflow/core": patch
---

Override setTimeout, setInterval, and related functions in workflow VM context to throw helpful errors suggesting to use `sleep` instead
3 changes: 3 additions & 0 deletions docs/content/docs/errors/index.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,6 +17,9 @@ Fix common mistakes when creating and executing workflows in the **Workflow DevK
<Card href="/docs/errors/start-invalid-workflow-function" title="start-invalid-workflow-function">
Learn how to start an invalid workflow function.
</Card>
<Card href="/docs/errors/timeout-in-workflow" title="timeout-in-workflow">
Learn how to handle timing delays in workflow functions.
</Card>
<Card href="/docs/errors/webhook-invalid-respond-with-value" title="webhook-invalid-respond-with-value">
Learn how to use the correct `respondWith` values for webhooks.
</Card>
Expand Down
120 changes: 120 additions & 0 deletions docs/content/docs/errors/timeout-in-workflow.mdx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
---
title: timeout-in-workflow
---

This error occurs when you try to use `setTimeout()`, `setInterval()`, or related timing functions directly inside a workflow function.

## Error Message

```
Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions. Use the "sleep" function from "workflow" for time-based delays.
```

## Why This Happens

Workflow functions run in a sandboxed environment where timing functions like `setTimeout()` and `setInterval()` are not available. These functions rely on asynchronous scheduling that would break the **deterministic replay** guarantees that workflows depend on.

When a workflow suspends and later resumes, it replays from the event log. If timing functions were allowed, the replay would produce different results than the original execution.

## Quick Fix

Use the `sleep` function from the `workflow` package for time-based delays. Unlike `setTimeout()`, `sleep` is tracked in the event log and replays correctly.

**Before:**

```typescript lineNumbers title="workflows/delayed.ts"
export async function delayedWorkflow() {
"use workflow";

// Error - setTimeout is not available in workflow functions
await new Promise(resolve => setTimeout(resolve, 5000)); // [!code highlight]

return 'done';
}
```

**After:**

```typescript lineNumbers title="workflows/delayed.ts"
import { sleep } from 'workflow'; // [!code highlight]

export async function delayedWorkflow() {
"use workflow";

// sleep is tracked in the event log and replays correctly
await sleep('5s'); // [!code highlight]

return 'done';
}
```

## Unavailable Functions

These timing functions cannot be used in workflow functions:

- `setTimeout()`
- `setInterval()`
- `setImmediate()`
- `clearTimeout()`
- `clearInterval()`
- `clearImmediate()`
Comment thread
TooTallNate marked this conversation as resolved.

## Common Scenarios

### Polling with Delays

If you need to poll an external service with delays between requests:

```typescript lineNumbers title="workflows/polling.ts"
import { sleep } from 'workflow';

export async function pollingWorkflow() {
"use workflow";

let status = 'pending';

while (status === 'pending') {
status = await checkStatus(); // step function
if (status === 'pending') {
await sleep('10s'); // [!code highlight]
}
}

return status;
}

async function checkStatus() {
"use step";
const response = await fetch('https://api.example.com/status');
const data = await response.json();
return data.status;
}
```

### Scheduled Delays

For workflows that need to wait for a specific duration:

```typescript lineNumbers title="workflows/reminder.ts"
import { sleep } from 'workflow';

export async function reminderWorkflow(message: string) {
"use workflow";

// Wait 24 hours before sending reminder
await sleep('24h'); // [!code highlight]

await sendReminder(message);

return 'reminder sent';
}

async function sendReminder(message: string) {
"use step";
// Send reminder logic
}
```

<Callout type="info">
The `sleep` function accepts duration strings like `'5s'`, `'10m'`, `'1h'`, `'24h'`, or milliseconds as a number. See the [sleep API reference](/docs/api-reference/workflow/sleep) for more details.
</Callout>
213 changes: 213 additions & 0 deletions packages/core/src/workflow.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -947,6 +947,219 @@ describe('runWorkflow', () => {
});
});

describe('timeout functions', () => {
it('should throw an error when calling setTimeout', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
setTimeout(() => {}, 1000);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should throw an error when calling setInterval', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
setInterval(() => {}, 1000);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should throw an error when calling clearTimeout', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
clearTimeout(123);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should throw an error when calling clearInterval', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
clearInterval(123);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should throw an error when calling setImmediate', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
setImmediate(() => {});
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should throw an error when calling clearImmediate', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
clearImmediate(123);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should include documentation link in error message', async () => {
let error: Error | undefined;
try {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await runWorkflow(
`async function workflow() {
setTimeout(() => {}, 1000);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
);
} catch (err) {
error = err as Error;
}
assert(error);
expect(error.message).toContain(
'https://useworkflow.dev/err/timeout-in-workflow'
);
expect(error.message).toContain(
'Use the "sleep" function from "workflow"'
);
});
});

describe('hook', () => {
it('should throw `WorkflowSuspension` when a hook is awaiting without a "hook_received" event', async () => {
let error: Error | undefined;
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
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
6 changes: 6 additions & 0 deletions .changeset/timeout-functions-error.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
---
"@workflow/errors": patch
"@workflow/core": patch
---

Override setTimeout, setInterval, and related functions in workflow VM context to throw helpful errors suggesting to use `sleep` instead
3 changes: 3 additions & 0 deletions docs/content/docs/errors/index.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,6 +17,9 @@ Fix common mistakes when creating and executing workflows in the **Workflow DevK
<Card href="/docs/errors/start-invalid-workflow-function" title="start-invalid-workflow-function">
Learn how to start an invalid workflow function.
</Card>
<Card href="/docs/errors/timeout-in-workflow" title="timeout-in-workflow">
Learn how to handle timing delays in workflow functions.
</Card>
<Card href="/docs/errors/webhook-invalid-respond-with-value" title="webhook-invalid-respond-with-value">
Learn how to use the correct `respondWith` values for webhooks.
</Card>
Expand Down
120 changes: 120 additions & 0 deletions docs/content/docs/errors/timeout-in-workflow.mdx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
---
title: timeout-in-workflow
---

This error occurs when you try to use `setTimeout()`, `setInterval()`, or related timing functions directly inside a workflow function.

## Error Message

```
Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions. Use the "sleep" function from "workflow" for time-based delays.
```

## Why This Happens

Workflow functions run in a sandboxed environment where timing functions like `setTimeout()` and `setInterval()` are not available. These functions rely on asynchronous scheduling that would break the **deterministic replay** guarantees that workflows depend on.

When a workflow suspends and later resumes, it replays from the event log. If timing functions were allowed, the replay would produce different results than the original execution.

## Quick Fix

Use the `sleep` function from the `workflow` package for time-based delays. Unlike `setTimeout()`, `sleep` is tracked in the event log and replays correctly.

**Before:**

```typescript lineNumbers title="workflows/delayed.ts"
export async function delayedWorkflow() {
"use workflow";

// Error - setTimeout is not available in workflow functions
await new Promise(resolve => setTimeout(resolve, 5000)); // [!code highlight]

return 'done';
}
```

**After:**

```typescript lineNumbers title="workflows/delayed.ts"
import { sleep } from 'workflow'; // [!code highlight]

export async function delayedWorkflow() {
"use workflow";

// sleep is tracked in the event log and replays correctly
await sleep('5s'); // [!code highlight]

return 'done';
}
```

## Unavailable Functions

These timing functions cannot be used in workflow functions:

- `setTimeout()`
- `setInterval()`
- `setImmediate()`
- `clearTimeout()`
- `clearInterval()`
- `clearImmediate()`
Comment thread
TooTallNate marked this conversation as resolved.

## Common Scenarios

### Polling with Delays

If you need to poll an external service with delays between requests:

```typescript lineNumbers title="workflows/polling.ts"
import { sleep } from 'workflow';

export async function pollingWorkflow() {
"use workflow";

let status = 'pending';

while (status === 'pending') {
status = await checkStatus(); // step function
if (status === 'pending') {
await sleep('10s'); // [!code highlight]
}
}

return status;
}

async function checkStatus() {
"use step";
const response = await fetch('https://api.example.com/status');
const data = await response.json();
return data.status;
}
```

### Scheduled Delays

For workflows that need to wait for a specific duration:

```typescript lineNumbers title="workflows/reminder.ts"
import { sleep } from 'workflow';

export async function reminderWorkflow(message: string) {
"use workflow";

// Wait 24 hours before sending reminder
await sleep('24h'); // [!code highlight]

await sendReminder(message);

return 'reminder sent';
}

async function sendReminder(message: string) {
"use step";
// Send reminder logic
}
```

<Callout type="info">
The `sleep` function accepts duration strings like `'5s'`, `'10m'`, `'1h'`, `'24h'`, or milliseconds as a number. See the [sleep API reference](/docs/api-reference/workflow/sleep) for more details.
</Callout>
213 changes: 213 additions & 0 deletions packages/core/src/workflow.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -947,6 +947,219 @@ describe('runWorkflow', () => {
});
});

describe('timeout functions', () => {
it('should throw an error when calling setTimeout', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
setTimeout(() => {}, 1000);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should throw an error when calling setInterval', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
setInterval(() => {}, 1000);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should throw an error when calling clearTimeout', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
clearTimeout(123);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should throw an error when calling clearInterval', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
clearInterval(123);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should throw an error when calling setImmediate', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
setImmediate(() => {});
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should throw an error when calling clearImmediate', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
clearImmediate(123);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should include documentation link in error message', async () => {
let error: Error | undefined;
try {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await runWorkflow(
`async function workflow() {
setTimeout(() => {}, 1000);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
);
} catch (err) {
error = err as Error;
}
assert(error);
expect(error.message).toContain(
'https://useworkflow.dev/err/timeout-in-workflow'
);
expect(error.message).toContain(
'Use the "sleep" function from "workflow"'
);
});
});

describe('hook', () => {
it('should throw `WorkflowSuspension` when a hook is awaiting without a "hook_received" event', async () => {
let error: Error | undefined;
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
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
6 changes: 6 additions & 0 deletions .changeset/timeout-functions-error.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
---
"@workflow/errors": patch
"@workflow/core": patch
---

Override setTimeout, setInterval, and related functions in workflow VM context to throw helpful errors suggesting to use `sleep` instead
3 changes: 3 additions & 0 deletions docs/content/docs/errors/index.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,6 +17,9 @@ Fix common mistakes when creating and executing workflows in the **Workflow DevK
<Card href="/docs/errors/start-invalid-workflow-function" title="start-invalid-workflow-function">
Learn how to start an invalid workflow function.
</Card>
<Card href="/docs/errors/timeout-in-workflow" title="timeout-in-workflow">
Learn how to handle timing delays in workflow functions.
</Card>
<Card href="/docs/errors/webhook-invalid-respond-with-value" title="webhook-invalid-respond-with-value">
Learn how to use the correct `respondWith` values for webhooks.
</Card>
Expand Down
120 changes: 120 additions & 0 deletions docs/content/docs/errors/timeout-in-workflow.mdx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
---
title: timeout-in-workflow
---

This error occurs when you try to use `setTimeout()`, `setInterval()`, or related timing functions directly inside a workflow function.

## Error Message

```
Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions. Use the "sleep" function from "workflow" for time-based delays.
```

## Why This Happens

Workflow functions run in a sandboxed environment where timing functions like `setTimeout()` and `setInterval()` are not available. These functions rely on asynchronous scheduling that would break the **deterministic replay** guarantees that workflows depend on.

When a workflow suspends and later resumes, it replays from the event log. If timing functions were allowed, the replay would produce different results than the original execution.

## Quick Fix

Use the `sleep` function from the `workflow` package for time-based delays. Unlike `setTimeout()`, `sleep` is tracked in the event log and replays correctly.

**Before:**

```typescript lineNumbers title="workflows/delayed.ts"
export async function delayedWorkflow() {
"use workflow";

// Error - setTimeout is not available in workflow functions
await new Promise(resolve => setTimeout(resolve, 5000)); // [!code highlight]

return 'done';
}
```

**After:**

```typescript lineNumbers title="workflows/delayed.ts"
import { sleep } from 'workflow'; // [!code highlight]

export async function delayedWorkflow() {
"use workflow";

// sleep is tracked in the event log and replays correctly
await sleep('5s'); // [!code highlight]

return 'done';
}
```

## Unavailable Functions

These timing functions cannot be used in workflow functions:

- `setTimeout()`
- `setInterval()`
- `setImmediate()`
- `clearTimeout()`
- `clearInterval()`
- `clearImmediate()`
Comment thread
TooTallNate marked this conversation as resolved.

## Common Scenarios

### Polling with Delays

If you need to poll an external service with delays between requests:

```typescript lineNumbers title="workflows/polling.ts"
import { sleep } from 'workflow';

export async function pollingWorkflow() {
"use workflow";

let status = 'pending';

while (status === 'pending') {
status = await checkStatus(); // step function
if (status === 'pending') {
await sleep('10s'); // [!code highlight]
}
}

return status;
}

async function checkStatus() {
"use step";
const response = await fetch('https://api.example.com/status');
const data = await response.json();
return data.status;
}
```

### Scheduled Delays

For workflows that need to wait for a specific duration:

```typescript lineNumbers title="workflows/reminder.ts"
import { sleep } from 'workflow';

export async function reminderWorkflow(message: string) {
"use workflow";

// Wait 24 hours before sending reminder
await sleep('24h'); // [!code highlight]

await sendReminder(message);

return 'reminder sent';
}

async function sendReminder(message: string) {
"use step";
// Send reminder logic
}
```

<Callout type="info">
The `sleep` function accepts duration strings like `'5s'`, `'10m'`, `'1h'`, `'24h'`, or milliseconds as a number. See the [sleep API reference](/docs/api-reference/workflow/sleep) for more details.
</Callout>
213 changes: 213 additions & 0 deletions packages/core/src/workflow.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -947,6 +947,219 @@ describe('runWorkflow', () => {
});
});

describe('timeout functions', () => {
it('should throw an error when calling setTimeout', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
setTimeout(() => {}, 1000);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should throw an error when calling setInterval', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
setInterval(() => {}, 1000);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should throw an error when calling clearTimeout', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
clearTimeout(123);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should throw an error when calling clearInterval', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
clearInterval(123);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should throw an error when calling setImmediate', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
setImmediate(() => {});
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should throw an error when calling clearImmediate', async () => {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await expect(
runWorkflow(
`async function workflow() {
clearImmediate(123);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
)
).rejects.toThrow(
'Timeout functions like "setTimeout" and "setInterval" are not supported in workflow functions'
);
});

it('should include documentation link in error message', async () => {
let error: Error | undefined;
try {
const ops: Promise<any>[] = [];
const workflowRun: WorkflowRun = {
runId: 'test-run-123',
workflowName: 'workflow',
status: 'running',
input: dehydrateWorkflowArguments([], ops),
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-01T00:00:00.000Z'),
startedAt: new Date('2024-01-01T00:00:00.000Z'),
deploymentId: 'test-deployment',
};

const events: Event[] = [];

await runWorkflow(
`async function workflow() {
setTimeout(() => {}, 1000);
return 'done';
}${getWorkflowTransformCode('workflow')}`,
workflowRun,
events
);
} catch (err) {
error = err as Error;
}
assert(error);
expect(error.message).toContain(
'https://useworkflow.dev/err/timeout-in-workflow'
);
expect(error.message).toContain(
'Use the "sleep" function from "workflow"'
);
});
});

describe('hook', () => {
it('should throw `WorkflowSuspension` when a hook is awaiting without a "hook_received" event', async () => {
let error: Error | undefined;
Expand Down
Loading