Workflows
Build durable, multi-step workflows with automatic retries and state persistence.
Workflows
Ploy Workflows let you define long-running, multi-step processes that survive failures. Each step is durably persisted and automatically retried on errors.
Configuration
Add a workflow binding in your ploy.yaml:
kind: worker
build: pnpm build
out: dist
workflow:
ORDER_FLOW: order_processingThe key (ORDER_FLOW) is the binding name. The value (order_processing) must match the workflow function name in your code.
Configuring Retries
By default, each step is retried up to 3 times on failure with exponential backoff. You can customize the default retry count per workflow in ploy.yaml:
workflow:
ORDER_FLOW:
name: order_processing
retries: 5 # Retry failed steps up to 5 times
FAST_FLOW:
name: quick_task
retries: 0 # No retries — fail immediately on errorYou can also override retries per step using step options:
const result = await step.run(
"call-external-api",
async () => {
return await callAPI();
},
{ retries: 10 }, // Override: retry this step up to 10 times
);Run ploy types to generate TypeScript types:
import type { WorkflowBinding } from "@meetploy/types";
export interface Env {
ORDER_FLOW: WorkflowBinding;
}Basic Example
export default {
async fetch(request, env) {
// Trigger a workflow execution
const { executionId } = await env.ORDER_FLOW.trigger({
orderId: "123",
amount: 99.99,
});
return Response.json({ executionId });
},
workflows: {
order_processing: {
handler: async ({
input,
step,
}: WorkflowContext<PloyEnv, { orderId: string; amount: number }>) => {
// Step 1: Validate
const order = await step.run("validate", async () => {
return { valid: true, orderId: input.orderId };
});
// Step 2: Charge payment
const payment = await step.run("charge", async () => {
return { paymentId: `pay_${Date.now()}` };
});
// Step 3: Fulfill
const fulfillment = await step.run("fulfill", async () => {
return { trackingNumber: `TRACK_${Date.now()}` };
});
return { orderId: order.orderId, paymentId: payment.paymentId };
},
},
},
} satisfies Ploy;Inputs and Outputs
Each workflow execution stores two layers of data:
- Execution input: the object you pass to
trigger() - Execution output: the final value returned by the workflow handler
Each step can also record its own snapshot data:
- Step input: optional metadata you pass through
step.run(..., { input }) - Step output: the value returned from the step callback
const validation = await step.run(
"validate",
async () => {
return {
valid: true,
orderId: input.orderId,
amountCents: Math.round(input.amount * 100),
};
},
{
input: {
orderId: input.orderId,
amount: input.amount,
},
},
);
return {
orderId: validation.orderId,
amountCents: validation.amountCents,
};Use input in the step options when you want the dashboard to show the exact
payload that reached a specific step. The step return value becomes the step
output snapshot.
Workflow Binding API
Trigger
Start a new workflow execution:
const { executionId } = await env.ORDER_FLOW.trigger({ orderId: "123" });Get Status
Check execution status:
const execution = await env.ORDER_FLOW.getExecution(executionId);
// { status: "running" | "completed" | "failed", result?: any }Cancel
Cancel a running execution:
await env.ORDER_FLOW.cancel(executionId);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 secondsSteps that throw are automatically retried up to 3 times by default
(configurable in ploy.yaml or per-step via options). Use unique step names
to ensure idempotency.
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: {
order_processing: {
handler: async ({ input, 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.
Workflow UI
The local dashboard lets you inspect both execution-level state and per-step progress after you trigger a workflow.


The execution detail view is where execution input/output, step input/output, retry attempts, and timing become visible. This example was triggered from examples/workflow-simple with a custom order payload.
That example now exposes both /trigger for a successful execution and /trigger-failing for a workflow that fails the charge_payment step after all retries, which is useful for inspecting the final failure state and retry history in the UI.


Next Steps
How is this guide?
Last updated on