Ploy
Ploy
Next.js

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:

ploy.yaml
kind: dynamic
build: pnpm build
out: dist
workflow:
  DATA_FLOW: data_processing

The key (DATA_FLOW) is the binding name. The value (data_processing) must match the workflow function name in your code.

Generate types:

pnpm ploy types

Triggering Workflows

Trigger workflows from API routes or Server Actions:

app/api/workflow/route.ts
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:

app/ploy.ts
interface WorkflowInput {
	action: string;
	value: number;
}

interface WorkflowOutput {
	action: string;
	originalValue: number;
	processedValue: number;
}

export default {
	workflows: {
		async data_processing({
			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;

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 seconds

Steps that throw are automatically retried. Use unique step names to ensure idempotency.

Local Development

During development, the local Ploy dashboard at http://localhost:4000 lets you:

  • View workflow executions
  • Inspect step results
  • Monitor execution status

Next Steps

How is this guide?

Last updated on