Workflows
Build durable multi-step processes in your Next.js app with Ploy workflows.
Workflows
Build long-running, multi-step processes that survive failures with automatic retries and state persistence.
Configuration
Add a workflow binding in your ploy.yaml:
kind: dynamic
build: pnpm build
out: dist
workflow:
DATA_FLOW: data_processingThe key (DATA_FLOW) is the binding name. The value (data_processing) must match the workflow function name in your code.
You can use the expanded form to configure retries per workflow:
workflow:
DATA_FLOW:
name: data_processing
retries: 5 # Retry failed steps up to 5 times (default: 3)Generate types:
pnpm ploy typesTriggering Workflows
Trigger workflows from API routes or Server Actions:
import { NextResponse } from "next/server";
import { getPloyContext } from "@meetploy/nextjs";
export async function POST(request: Request) {
const { env } = getPloyContext();
const body = await request.json();
const { executionId } = await env.DATA_FLOW.trigger({
action: body.action,
value: body.value,
});
return NextResponse.json({ success: true, executionId });
}Get Execution Status
export async function GET(request: Request) {
const { env } = getPloyContext();
const url = new URL(request.url);
const executionId = url.searchParams.get("executionId");
const execution = await env.DATA_FLOW.getExecution(executionId);
return NextResponse.json({
id: execution.id,
status: execution.status, // "running" | "completed" | "failed"
output: execution.output,
});
}Cancel Execution
await env.DATA_FLOW.cancel(executionId);Workflow Definition
Define workflows in your ploy.ts file:
interface WorkflowInput {
action: string;
value: number;
}
interface WorkflowOutput {
action: string;
originalValue: number;
processedValue: number;
}
export default {
workflows: {
data_processing: {
handler: async ({
input,
env,
step,
}: WorkflowContext<PloyEnv, WorkflowInput>): Promise<WorkflowOutput> => {
// Step 1: Validate input
const validation = await step.run("validate", async () => {
if (!input.action) {
throw new Error("Missing action");
}
return { valid: true, action: input.action };
});
// Step 2: Process the value
const processed = await step.run("process", async () => {
let result: number;
switch (input.action) {
case "double":
result = input.value * 2;
break;
case "square":
result = input.value * input.value;
break;
default:
result = input.value;
}
return { processedValue: result };
});
// Step 3: Store result
await step.run("store", async () => {
await env.DB.prepare(
"INSERT INTO results (action, input, output) VALUES (?, ?, ?)",
)
.bind(input.action, input.value, processed.processedValue)
.run();
});
return {
action: input.action,
originalValue: input.value,
processedValue: processed.processedValue,
};
},
},
},
} satisfies Ploy;Inputs and Outputs
There are two levels of persisted workflow data:
- Execution input: the payload passed to
env.DATA_FLOW.trigger(...) - Execution output: the final value returned by the workflow handler
For deeper debugging, each step can also persist its own snapshots:
- Step input: optional metadata passed with
step.run(..., { input }) - Step output: the value returned from the step callback
const processed = await step.run(
"process",
async () => {
return { processedValue: input.value * 2 };
},
{
input: {
action: input.action,
value: input.value,
},
},
);This is especially useful when a workflow transforms data between steps and you want the dashboard to show what each step actually received.
Step API
step.run
Execute a named step. Results are persisted, so re-runs skip completed steps:
const result = await step.run("step-name", async () => {
return { data: "value" };
});step.sleep
Pause execution for a duration (in milliseconds):
await step.sleep(5000); // Wait 5 secondsGenerated worker runtimes use the configured callback retry default (normally
three). The Next.js development route defaults to zero; set per-step
{ retries: 3 } explicitly when testing callback retries there. Use
{ retries: false } (or { retries: 0 }) for non-idempotent effects.
Callback retries and completion acknowledgements are separate. Once a callback succeeds, transient completion failures retry only the acknowledgement, at most twice more with the same serialized output. Every acknowledgement checks the HTTP status and success response. Exhausted or rejected acknowledgements surface an infrastructure error without rerunning successful callbacks, invoking onError, or marking the workflow failed. Workflow completion follows the same rule.
This does not guarantee exactly-once effects across a crash: an external operation may succeed before its completion is persisted. Even with retries: false, a later replay can execute an uncommitted step again. Use provider idempotency keys or reconcile uncertain effects. Unique step names identify persisted results, not atomic external side effects. Reported acknowledgement uncertainty holds the running execution out of automatic failure/recovery until investigation or explicit cancellation. An unreported host failure or a lost entire execution response can still trigger best-effort recovery and replay uncommitted work.
With suspendable dispatch, sleep persists a wake-up deadline and returns control to the broker/emulator until that deadline. Replay reuses the persisted deadline and skips completed elapsed sleeps. Legacy non-suspendable dispatch still uses a process-local timer and is not host-failure durable.
Error Handling
Each workflow definition supports an optional onError callback that runs when the handler throws. Use it for cleanup, alerting, or releasing locks:
workflows: {
data_processing: {
handler: async ({ input, env, step }) => {
// ... workflow steps
},
onError: async (error, { env }) => {
console.error("Workflow failed:", error.message);
// Release locks, send alerts, clean up resources
},
},
},The onError hook runs before the workflow is marked as failed. If onError itself throws, the error is ignored and the workflow still fails normally.
Locally, workflow steps are dispatched to your app's /ploy-handler route,
which initPloyForDev() creates for you. See Background
Handlers.
Local Development
During development, the local Ploy dashboard at http://localhost:4000 lets you:
- View workflow executions
- Inspect step results
- Monitor execution status


Open an execution to inspect execution input/output, step-level input/output, retry attempts, and timing details.


Next Steps
How is this guide?
Last updated on