workflow 5.0.0-beta.0 → 5.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +4 -4
  2. package/dist/api-workflow.d.ts +1 -3
  3. package/dist/api-workflow.d.ts.map +1 -1
  4. package/dist/api-workflow.js +2 -6
  5. package/dist/api.js +1 -1
  6. package/dist/astro.js +1 -1
  7. package/dist/index.js +1 -1
  8. package/dist/internal/builtins.js +1 -1
  9. package/dist/internal/class-serialization.js +1 -1
  10. package/dist/internal/errors.js +1 -1
  11. package/dist/nest.js +1 -1
  12. package/dist/next.cjs +4 -2
  13. package/dist/next.d.cts +1 -1
  14. package/dist/next.d.cts.map +1 -1
  15. package/dist/nitro.js +1 -1
  16. package/dist/nuxt.js +1 -1
  17. package/dist/observability.d.ts +1 -1
  18. package/dist/observability.js +2 -2
  19. package/dist/runtime.js +1 -1
  20. package/dist/stdlib.js +1 -1
  21. package/dist/sveltekit.js +1 -1
  22. package/dist/typescript-plugin.cjs +1 -1
  23. package/dist/vite.js +1 -1
  24. package/dist/workflow.js +1 -1
  25. package/docs/ai/resumable-streams.mdx +1 -1
  26. package/docs/api-reference/workflow/create-webhook.mdx +37 -18
  27. package/docs/api-reference/workflow/get-workflow-metadata.mdx +61 -0
  28. package/docs/api-reference/workflow-ai/durable-agent.mdx +0 -4
  29. package/docs/api-reference/workflow-ai/index.mdx +0 -5
  30. package/docs/api-reference/workflow-ai/workflow-chat-transport.mdx +0 -4
  31. package/docs/api-reference/workflow-api/get-world.mdx +6 -6
  32. package/docs/api-reference/workflow-api/index.mdx +1 -1
  33. package/docs/api-reference/workflow-api/world/index.mdx +2 -2
  34. package/docs/api-reference/workflow-api/world/observability.mdx +1 -1
  35. package/docs/api-reference/workflow-api/world/queue.mdx +1 -1
  36. package/docs/api-reference/workflow-api/world/storage.mdx +8 -8
  37. package/docs/api-reference/workflow-api/world/streams.mdx +38 -36
  38. package/docs/cookbook/advanced/custom-serialization.mdx +168 -0
  39. package/docs/cookbook/advanced/durable-objects.mdx +148 -0
  40. package/docs/cookbook/advanced/isomorphic-packages.mdx +145 -0
  41. package/docs/cookbook/advanced/meta.json +10 -0
  42. package/docs/cookbook/advanced/publishing-libraries.mdx +279 -0
  43. package/docs/cookbook/advanced/serializable-steps.mdx +135 -0
  44. package/docs/cookbook/agent-patterns/durable-agent.mdx +191 -0
  45. package/docs/cookbook/agent-patterns/human-in-the-loop.mdx +278 -0
  46. package/docs/cookbook/agent-patterns/meta.json +10 -0
  47. package/docs/cookbook/agent-patterns/stop-workflow.mdx +216 -0
  48. package/docs/cookbook/agent-patterns/tool-orchestration.mdx +255 -0
  49. package/docs/cookbook/agent-patterns/tool-streaming.mdx +181 -0
  50. package/docs/cookbook/common-patterns/batching.mdx +179 -0
  51. package/docs/cookbook/common-patterns/child-workflows.mdx +372 -0
  52. package/docs/cookbook/common-patterns/content-router.mdx +207 -0
  53. package/docs/cookbook/common-patterns/fan-out.mdx +208 -0
  54. package/docs/cookbook/common-patterns/idempotency.mdx +107 -0
  55. package/docs/cookbook/common-patterns/meta.json +15 -0
  56. package/docs/cookbook/common-patterns/rate-limiting.mdx +228 -0
  57. package/docs/cookbook/common-patterns/saga.mdx +152 -0
  58. package/docs/cookbook/common-patterns/scheduling.mdx +249 -0
  59. package/docs/cookbook/common-patterns/webhooks.mdx +185 -0
  60. package/docs/cookbook/index.mdx +41 -0
  61. package/docs/cookbook/integrations/ai-sdk.mdx +204 -0
  62. package/docs/cookbook/integrations/chat-sdk.mdx +203 -0
  63. package/docs/cookbook/integrations/meta.json +4 -0
  64. package/docs/cookbook/integrations/sandbox.mdx +128 -0
  65. package/docs/cookbook/meta.json +5 -0
  66. package/docs/deploying/building-a-world.mdx +45 -43
  67. package/docs/deploying/world/local-world.mdx +1 -1
  68. package/docs/deploying/world/postgres-world.mdx +10 -5
  69. package/docs/deploying/world/vercel-world.mdx +1 -1
  70. package/docs/errors/start-invalid-workflow-function.mdx +1 -1
  71. package/docs/getting-started/index.mdx +8 -1
  72. package/docs/getting-started/meta.json +2 -1
  73. package/docs/getting-started/next.mdx +24 -0
  74. package/docs/getting-started/python.mdx +165 -0
  75. package/docs/how-it-works/code-transform.mdx +6 -5
  76. package/docs/meta.json +1 -0
  77. package/docs/migration-guides/index.mdx +34 -0
  78. package/docs/migration-guides/meta.json +9 -0
  79. package/docs/migration-guides/migrating-from-aws-step-functions.mdx +311 -0
  80. package/docs/migration-guides/migrating-from-inngest.mdx +282 -0
  81. package/docs/migration-guides/migrating-from-temporal.mdx +284 -0
  82. package/docs/migration-guides/migrating-from-trigger-dev.mdx +296 -0
  83. package/package.json +13 -14
  84. package/dist/internal/private.d.ts +0 -6
  85. package/dist/internal/private.d.ts.map +0 -1
  86. package/dist/internal/private.js +0 -6
@@ -0,0 +1,278 @@
1
+ ---
2
+ title: Human-in-the-Loop
3
+ description: Pause an AI agent to wait for human approval, then resume based on the decision.
4
+ type: guide
5
+ summary: Use defineHook with the tool call ID to suspend an agent for human approval, with an optional timeout.
6
+ ---
7
+
8
+ Use this pattern when an AI agent needs human confirmation before performing a consequential action like booking, purchasing, or publishing. The workflow suspends without consuming resources until the human responds.
9
+
10
+ ## Pattern
11
+
12
+ Create a typed hook using `defineHook()`. When the agent calls the approval tool, the tool creates a hook instance using the tool call ID as the token, then awaits it. The UI renders approval controls, and an API route resumes the hook with the decision.
13
+
14
+ ### Simplified
15
+
16
+ ```typescript lineNumbers
17
+ import { DurableAgent } from "@workflow/ai/agent";
18
+ import { defineHook, sleep, getWritable } from "workflow";
19
+ import { z } from "zod";
20
+ import type { ModelMessage, UIMessageChunk } from "ai";
21
+
22
+ export const bookingApprovalHook = defineHook({
23
+ schema: z.object({
24
+ approved: z.boolean(),
25
+ comment: z.string().optional(),
26
+ }),
27
+ });
28
+
29
+ declare function confirmBooking(args: { flightId: string; passenger: string }): Promise<{ confirmationId: string }>; // @setup
30
+
31
+ // This tool runs at the workflow level (no "use step") because hooks are workflow primitives
32
+ async function requestBookingApproval(
33
+ { flightId, passenger, price }: { flightId: string; passenger: string; price: number },
34
+ { toolCallId }: { toolCallId: string }
35
+ ) {
36
+ const hook = bookingApprovalHook.create({ token: toolCallId }); // [!code highlight]
37
+
38
+ const result = await Promise.race([ // [!code highlight]
39
+ hook.then((payload) => ({ type: "decision" as const, ...payload })),
40
+ sleep("24h").then(() => ({ type: "timeout" as const, approved: false })),
41
+ ]);
42
+
43
+ if (result.type === "timeout") return "Booking request expired after 24 hours.";
44
+ if (!result.approved) return `Booking rejected: ${result.comment || "No reason given"}`;
45
+
46
+ const booking = await confirmBooking({ flightId, passenger });
47
+ return `Booked! Confirmation: ${booking.confirmationId}`;
48
+ }
49
+
50
+ export async function bookingAgent(messages: ModelMessage[]) {
51
+ "use workflow";
52
+
53
+ const agent = new DurableAgent({
54
+ model: "anthropic/claude-haiku-4.5",
55
+ instructions: "You help book flights. Always request approval before booking.",
56
+ tools: {
57
+ requestBookingApproval: {
58
+ description: "Request human approval before booking a flight",
59
+ inputSchema: z.object({
60
+ flightId: z.string(),
61
+ passenger: z.string(),
62
+ price: z.number(),
63
+ }),
64
+ execute: requestBookingApproval,
65
+ },
66
+ },
67
+ });
68
+
69
+ await agent.stream({ // [!code highlight]
70
+ messages,
71
+ writable: getWritable<UIMessageChunk>(),
72
+ });
73
+ }
74
+ ```
75
+
76
+ ### Full Implementation
77
+
78
+ ```typescript lineNumbers
79
+ import { DurableAgent } from "@workflow/ai/agent";
80
+ import { defineHook, sleep, getWritable } from "workflow";
81
+ import { z } from "zod";
82
+ import type { ModelMessage, UIMessageChunk } from "ai";
83
+
84
+ // Define the approval hook with schema validation
85
+ export const bookingApprovalHook = defineHook({
86
+ schema: z.object({
87
+ approved: z.boolean(),
88
+ comment: z.string().optional(),
89
+ }),
90
+ });
91
+
92
+ // Step: Search for flights (full Node.js access, automatic retries)
93
+ async function searchFlights({
94
+ from,
95
+ to,
96
+ date,
97
+ }: {
98
+ from: string;
99
+ to: string;
100
+ date: string;
101
+ }) {
102
+ "use step";
103
+
104
+ // Your real flight search API call here
105
+ await new Promise((resolve) => setTimeout(resolve, 500));
106
+ return {
107
+ flights: [
108
+ { id: "FL-100", airline: "Example Air", price: 299, from, to, date },
109
+ { id: "FL-200", airline: "Demo Airlines", price: 349, from, to, date },
110
+ ],
111
+ };
112
+ }
113
+
114
+ // Step: Confirm the booking after approval
115
+ async function confirmBooking({
116
+ flightId,
117
+ passenger,
118
+ }: {
119
+ flightId: string;
120
+ passenger: string;
121
+ }) {
122
+ "use step";
123
+
124
+ await new Promise((resolve) => setTimeout(resolve, 500));
125
+ return { confirmationId: `CONF-${flightId}-${Date.now().toString(36)}` };
126
+ }
127
+
128
+ // Workflow-level tool: hooks must be created in workflow context, not inside steps
129
+ async function requestBookingApproval(
130
+ {
131
+ flightId,
132
+ passenger,
133
+ price,
134
+ }: { flightId: string; passenger: string; price: number },
135
+ { toolCallId }: { toolCallId: string }
136
+ ) {
137
+ // No "use step" — hooks are workflow-level primitives
138
+
139
+ const hook = bookingApprovalHook.create({ token: toolCallId }); // [!code highlight]
140
+
141
+ // Race: human approval vs. 24-hour timeout
142
+ const result = await Promise.race([ // [!code highlight]
143
+ hook.then((payload) => ({ type: "decision" as const, ...payload })),
144
+ sleep("24h").then(() => ({ type: "timeout" as const, approved: false })),
145
+ ]);
146
+
147
+ if (result.type === "timeout") {
148
+ return "Booking request expired after 24 hours.";
149
+ }
150
+
151
+ if (!result.approved) {
152
+ return `Booking rejected: ${result.comment || "No reason given"}`;
153
+ }
154
+
155
+ // Approved — proceed with booking
156
+ const booking = await confirmBooking({ flightId, passenger });
157
+ return `Flight ${flightId} booked for ${passenger}. Confirmation: ${booking.confirmationId}`;
158
+ }
159
+
160
+ export async function bookingAgent(messages: ModelMessage[]) {
161
+ "use workflow";
162
+
163
+ const writable = getWritable<UIMessageChunk>();
164
+
165
+ const agent = new DurableAgent({
166
+ model: "anthropic/claude-haiku-4.5",
167
+ instructions:
168
+ "You are a flight booking assistant. Search for flights, then request approval before booking.",
169
+ tools: {
170
+ searchFlights: {
171
+ description: "Search for available flights",
172
+ inputSchema: z.object({
173
+ from: z.string().describe("Departure airport code"),
174
+ to: z.string().describe("Arrival airport code"),
175
+ date: z.string().describe("Travel date (YYYY-MM-DD)"),
176
+ }),
177
+ execute: searchFlights,
178
+ },
179
+ requestBookingApproval: {
180
+ description: "Request human approval before booking a flight",
181
+ inputSchema: z.object({
182
+ flightId: z.string().describe("Flight ID to book"),
183
+ passenger: z.string().describe("Passenger name"),
184
+ price: z.number().describe("Total price"),
185
+ }),
186
+ execute: requestBookingApproval,
187
+ },
188
+ },
189
+ });
190
+
191
+ await agent.stream({ messages, writable }); // [!code highlight]
192
+ }
193
+ ```
194
+
195
+ ### API Route for Approvals
196
+
197
+ ```typescript lineNumbers
198
+ import { bookingApprovalHook } from "@/workflows/booking-agent";
199
+
200
+ export async function POST(request: Request) {
201
+ const { toolCallId, approved, comment } = await request.json();
202
+
203
+ // Schema validation happens automatically via defineHook
204
+ await bookingApprovalHook.resume(toolCallId, { approved, comment }); // [!code highlight]
205
+
206
+ return Response.json({ success: true });
207
+ }
208
+ ```
209
+
210
+ ### Approval Component
211
+
212
+ ```tsx lineNumbers
213
+ "use client";
214
+
215
+ import { useState } from "react";
216
+
217
+ export function BookingApproval({
218
+ toolCallId,
219
+ input,
220
+ output,
221
+ }: {
222
+ toolCallId: string;
223
+ input?: { flightId: string; passenger: string; price: number };
224
+ output?: string;
225
+ }) {
226
+ const [comment, setComment] = useState("");
227
+ const [isSubmitting, setIsSubmitting] = useState(false);
228
+
229
+ if (output) {
230
+ return <p className="text-sm text-muted-foreground">{output}</p>;
231
+ }
232
+
233
+ const handleSubmit = async (approved: boolean) => {
234
+ setIsSubmitting(true);
235
+ await fetch("/api/hooks/approval", {
236
+ method: "POST",
237
+ headers: { "Content-Type": "application/json" },
238
+ body: JSON.stringify({ toolCallId, approved, comment }),
239
+ });
240
+ setIsSubmitting(false);
241
+ };
242
+
243
+ return (
244
+ <div className="border rounded-lg p-4 space-y-3">
245
+ {input && (
246
+ <div className="text-sm space-y-1">
247
+ <div>Flight: {input.flightId}</div>
248
+ <div>Passenger: {input.passenger}</div>
249
+ <div>Price: ${input.price}</div>
250
+ </div>
251
+ )}
252
+ <textarea
253
+ value={comment}
254
+ onChange={(e) => setComment(e.target.value)}
255
+ placeholder="Add a comment (optional)..."
256
+ className="w-full border rounded p-2 text-sm"
257
+ rows={2}
258
+ />
259
+ <div className="flex gap-2">
260
+ <button type="button" onClick={() => handleSubmit(true)} disabled={isSubmitting}>
261
+ Approve
262
+ </button>
263
+ <button type="button" onClick={() => handleSubmit(false)} disabled={isSubmitting}>
264
+ Reject
265
+ </button>
266
+ </div>
267
+ </div>
268
+ );
269
+ }
270
+ ```
271
+
272
+ ## Key APIs
273
+
274
+ - [`"use workflow"`](/docs/api-reference/workflow/use-workflow) — declares the orchestrator function
275
+ - [`"use step"`](/docs/api-reference/workflow/use-step) — declares step functions with retries
276
+ - [`defineHook()`](/docs/api-reference/workflow/define-hook) — type-safe hook with schema validation
277
+ - [`sleep()`](/docs/api-reference/workflow/sleep) — durable timeout for approval expiry
278
+ - [`DurableAgent`](/docs/api-reference/workflow-ai/durable-agent) — durable agent with tool definitions
@@ -0,0 +1,10 @@
1
+ {
2
+ "title": "Agent Patterns",
3
+ "pages": [
4
+ "durable-agent",
5
+ "tool-streaming",
6
+ "human-in-the-loop",
7
+ "tool-orchestration",
8
+ "stop-workflow"
9
+ ]
10
+ }
@@ -0,0 +1,216 @@
1
+ ---
2
+ title: Stop Workflow
3
+ description: Gracefully cancel a running agent workflow using a hook signal.
4
+ type: guide
5
+ summary: Use a hook as a stop signal to break out of an agent loop and close the stream cleanly.
6
+ ---
7
+
8
+ Use this pattern when you need to gracefully stop a running agent from the outside — for example, a "Stop" button in a chat UI or an admin cancellation endpoint. The workflow listens for a stop signal via a hook while the agent runs, and breaks out of the loop when the signal arrives.
9
+
10
+ ## Pattern
11
+
12
+ Create a hook with a known token (the run ID). Listen for a stop signal in a non-blocking `.then()`. In the `prepareStep` callback, check the flag and return `{ toolChoice: "none" }` to prevent further tool calls, causing the agent to generate a final response and exit the loop cleanly.
13
+
14
+ ### Simplified
15
+
16
+ ```typescript lineNumbers
17
+ import { DurableAgent } from "@workflow/ai/agent";
18
+ import { defineHook, getWritable, getWorkflowMetadata } from "workflow";
19
+ import { z } from "zod";
20
+ import type { ModelMessage, UIMessageChunk } from "ai";
21
+
22
+ export const stopHook = defineHook({
23
+ schema: z.object({ reason: z.string().optional() }),
24
+ });
25
+
26
+ declare function processQuery(args: { query: string }): Promise<string>; // @setup
27
+
28
+ export async function stoppableAgent(messages: ModelMessage[]) {
29
+ "use workflow";
30
+
31
+ const { workflowRunId } = getWorkflowMetadata();
32
+ let stopRequested = false;
33
+
34
+ const hook = stopHook.create({ token: `stop:${workflowRunId}` }); // [!code highlight]
35
+ hook.then(() => { stopRequested = true; }); // [!code highlight]
36
+
37
+ const agent = new DurableAgent({
38
+ model: "anthropic/claude-haiku-4.5",
39
+ tools: {
40
+ processQuery: {
41
+ description: "Process a query",
42
+ inputSchema: z.object({ query: z.string() }),
43
+ execute: processQuery,
44
+ },
45
+ },
46
+ });
47
+
48
+ const result = await agent.stream({
49
+ messages,
50
+ writable: getWritable<UIMessageChunk>(),
51
+ prepareStep: () => { // [!code highlight]
52
+ if (stopRequested) return { toolChoice: "none" }; // [!code highlight]
53
+ return {};
54
+ },
55
+ });
56
+
57
+ return { messages: result.messages, stopped: stopRequested };
58
+ }
59
+ ```
60
+
61
+ ### Full Implementation
62
+
63
+ ```typescript lineNumbers
64
+ import { DurableAgent } from "@workflow/ai/agent";
65
+ import { defineHook, getWritable, getWorkflowMetadata } from "workflow";
66
+ import { z } from "zod";
67
+ import type { ModelMessage, UIMessageChunk } from "ai";
68
+
69
+ // Hook to signal the workflow to stop
70
+ export const stopHook = defineHook({
71
+ schema: z.object({
72
+ reason: z.string().optional(),
73
+ }),
74
+ });
75
+
76
+ // Step: Search the web
77
+ async function searchWeb({ query }: { query: string }) {
78
+ "use step";
79
+
80
+ await new Promise((resolve) => setTimeout(resolve, 1000));
81
+ return { results: [`Result for "${query}"`] };
82
+ }
83
+
84
+ // Step: Analyze data
85
+ async function analyzeData({ data }: { data: string }) {
86
+ "use step";
87
+
88
+ await new Promise((resolve) => setTimeout(resolve, 800));
89
+ return { analysis: `Analysis of: ${data}` };
90
+ }
91
+
92
+ // Step: Write the final close marker to the stream
93
+ async function closeStream() {
94
+ "use step";
95
+
96
+ const writable = getWritable<UIMessageChunk>();
97
+ const writer = writable.getWriter();
98
+ try {
99
+ await writer.write({ type: "finish" } as UIMessageChunk);
100
+ } finally {
101
+ writer.releaseLock();
102
+ }
103
+ await writable.close();
104
+ }
105
+
106
+ export async function stoppableAgent(messages: ModelMessage[]) {
107
+ "use workflow";
108
+
109
+ const { workflowRunId } = getWorkflowMetadata();
110
+ const writable = getWritable<UIMessageChunk>();
111
+
112
+ // Listen for stop signal using a non-blocking hook
113
+ let stopRequested = false;
114
+ let stopReason: string | undefined;
115
+
116
+ const hook = stopHook.create({ token: `stop:${workflowRunId}` }); // [!code highlight]
117
+ hook.then(({ reason }) => { // [!code highlight]
118
+ stopRequested = true;
119
+ stopReason = reason;
120
+ });
121
+
122
+ const agent = new DurableAgent({
123
+ model: "anthropic/claude-haiku-4.5",
124
+ instructions: "You are a research assistant. Search and analyze data as needed.",
125
+ tools: {
126
+ searchWeb: {
127
+ description: "Search the web for information",
128
+ inputSchema: z.object({ query: z.string() }),
129
+ execute: searchWeb,
130
+ },
131
+ analyzeData: {
132
+ description: "Analyze a piece of data",
133
+ inputSchema: z.object({ data: z.string() }),
134
+ execute: analyzeData,
135
+ },
136
+ },
137
+ });
138
+
139
+ const result = await agent.stream({
140
+ messages,
141
+ writable,
142
+ preventClose: true,
143
+ maxSteps: 20,
144
+ prepareStep: ({ stepNumber }) => { // [!code highlight]
145
+ // Check stop flag before each agent step.
146
+ // Setting toolChoice to "none" prevents tool calls,
147
+ // causing the agent to generate a final response and exit.
148
+ if (stopRequested) {
149
+ return { toolChoice: "none" }; // [!code highlight]
150
+ }
151
+ return {};
152
+ },
153
+ });
154
+
155
+ // Clean up: close the stream
156
+ await closeStream();
157
+
158
+ return {
159
+ messages: result.messages,
160
+ stopped: stopRequested,
161
+ stopReason,
162
+ stepsCompleted: result.steps.length,
163
+ };
164
+ }
165
+ ```
166
+
167
+ ### API Route to Trigger Stop
168
+
169
+ ```typescript lineNumbers
170
+ import { stopHook } from "@/workflows/stoppable-agent";
171
+
172
+ export async function POST(
173
+ request: Request,
174
+ { params }: { params: Promise<{ runId: string }> }
175
+ ) {
176
+ const { runId } = await params;
177
+ const { reason } = await request.json();
178
+
179
+ await stopHook.resume(`stop:${runId}`, { // [!code highlight]
180
+ reason: reason || "User requested stop",
181
+ });
182
+
183
+ return Response.json({ success: true });
184
+ }
185
+ ```
186
+
187
+ ### Client Stop Button
188
+
189
+ ```tsx lineNumbers
190
+ "use client";
191
+
192
+ export function StopButton({ runId }: { runId: string }) {
193
+ const handleStop = async () => {
194
+ await fetch(`/api/chat/${runId}/stop`, {
195
+ method: "POST",
196
+ headers: { "Content-Type": "application/json" },
197
+ body: JSON.stringify({ reason: "User clicked stop" }),
198
+ });
199
+ };
200
+
201
+ return (
202
+ <button type="button" onClick={handleStop}>
203
+ Stop Agent
204
+ </button>
205
+ );
206
+ }
207
+ ```
208
+
209
+ ## Key APIs
210
+
211
+ - [`"use workflow"`](/docs/api-reference/workflow/use-workflow) — declares the orchestrator function
212
+ - [`"use step"`](/docs/api-reference/workflow/use-step) — declares step functions with retries
213
+ - [`defineHook()`](/docs/api-reference/workflow/define-hook) — type-safe hook for the stop signal
214
+ - [`getWorkflowMetadata()`](/docs/api-reference/workflow/get-workflow-metadata) — access the run ID for deterministic hook tokens
215
+ - [`getWritable()`](/docs/api-reference/workflow/get-writable) — stream output and close cleanly on stop
216
+ - [`DurableAgent`](/docs/api-reference/workflow-ai/durable-agent) — `prepareStep` callback to check stop flag before each step