| name | output-error-nondeterminism |
| description | Fix non-determinism errors in Output SDK workflows. Use when seeing replay failures, inconsistent results between runs, "non-deterministic" error messages, or workflows behaving differently on retry. |
| allowed-tools | Bash, Read |
Fix Non-Determinism Errors
Overview
This skill helps diagnose and fix non-determinism errors in Output SDK workflows. Workflows must be deterministic because Temporal may replay them during recovery or retries, and the replay must produce identical results.
When to Use This Skill
You're seeing:
- "non-deterministic" error messages
- Replay failures after workflow restart
- Inconsistent results between runs with same input
- Errors during workflow recovery
- Warnings about determinism violations
Root Cause
Temporal workflows must be deterministic: given the same input, they must always execute the same sequence of operations. This is because Temporal replays workflow history to recover state after crashes or restarts.
Non-deterministic operations break this replay mechanism because they produce different values each time.
Common Causes and Solutions
1. Math.random()
Problem: Random values differ on each execution.
// WRONG: Non-deterministic
export default workflow({
fn: async (input) => {
const id = Math.random().toString(36); // Different each time!
return await processWithId({ id });
}
});
Solution: Pass random values as workflow input or generate in a step.
// Option 1: Pass as input
export default workflow({
inputSchema: z.object({
id: z.string() // Generate ID before calling workflow
}),
fn: async (input) => {
return await processWithId({ id: input.id });
}
});
// Option 2: Generate in a step (steps can be non-deterministic)
export const generateId = step({
name: 'generateId',
fn: async () => ({ id: Math.random().toString(36) })
});
export default workflow({
fn: async (input) => {
const { id } = await generateId({});
return await processWithId({ id });
}
});
2. Date.now() / new Date()
Problem: Timestamps change between executions.
// WRONG: Non-deterministic
export default workflow({
fn: async (input) => {
const timestamp = Date.now(); // Different each replay!
return await logEvent({ timestamp });
}
});
Solution: Pass timestamps as input or use Temporal's time API.
// Option 1: Pass as input
export default workflow({
inputSchema: z.object({
timestamp: z.number()
}),
fn: async (input) => {
return await logEvent({ timestamp: input.timestamp });
}
});
// Option 2: Generate in a step
export const getTimestamp = step({
name: 'getTimestamp',
fn: async () => ({ timestamp: Date.now() })
});
3. crypto.randomUUID()
Problem: UUIDs differ each execution.
// WRONG: Non-deterministic
import { randomUUID } from 'crypto';
export default workflow({
fn: async (input) => {
const requestId = randomUUID(); // Different each time!
return await makeRequest({ requestId });
}
});
Solution: Generate UUIDs as input or in steps.
// Correct: Generate in step
export const generateRequestId = step({
name: 'generateRequestId',
fn: async () => {
const { randomUUID } = await import('crypto');
return { requestId: randomUUID() };
}
});
4. Dynamic Imports
Problem: Dynamic imports may resolve differently.
// WRONG: Non-deterministic import timing
export default workflow({
fn: async (input) => {
const module = await import(`./handlers/${input.type}`);
return module.handle(input);
}
});
Solution: Use static imports and conditional logic.
// Correct: Static imports with conditional use
import { handleTypeA } from './handlers/typeA';
import { handleTypeB } from './handlers/typeB';
export default workflow({
fn: async (input) => {
if (input.type === 'A') {
return await handleTypeA(input);
} else {
return await handleTypeB(input);
}
}
});
5. Environment Variables
Problem: Environment may differ between replays.
// WRONG: Environment can change
export default workflow({
fn: async (input) => {
const apiUrl = process.env.API_URL; // May differ on different workers
return await callApi({ url: apiUrl });
}
});
Solution: Pass configuration as input or use constants.
// Correct: Pass as input
export default workflow({
inputSchema: z.object({
apiUrl: z.string()
}),
fn: async (input) => {
return await callApi({ url: input.apiUrl });
}
});
How to Find Non-Deterministic Code
Search for Common Patterns
# Find Math.random usage
grep -rn "Math.random" src/workflows/
# Find Date.now or new Date
grep -rn "Date.now\|new Date" src/workflows/
# Find crypto random functions
grep -rn "randomUUID\|randomBytes" src/workflows/
# Find dynamic imports
grep -rn "import(" src/workflows/
Review Workflow Files
Look at your workflow fn functions specifically. Non-deterministic code is only a problem in workflow functions, not in step functions.
Verification Steps
- Fix the code using solutions above
- Run the workflow:
npx output workflow run <name> '<input>' - Run again with same input: Result should be identical
- Check for errors: No "non-deterministic" messages
The Determinism Rule
Workflow functions must be deterministic:
- Same input = same execution path
- No side effects (network, filesystem, random values)
- Only orchestration logic and step calls
Step functions can be non-deterministic:
- Steps record their results in Temporal history
- Replays use recorded results, not re-execution
- All I/O should happen in steps
Debugging Tip
If unsure whether code is causing issues:
# Run the workflow
npx output workflow start my-workflow '{"input": "test"}'
# Get the workflow ID and run debug to see replay behavior
npx output workflow debug <workflowId> --format json
Look for errors or warnings about non-determinism in the trace.
Related Issues
- For I/O in workflow code, see
output-error-direct-io - For random values needed in logic, generate them in steps or pass as input