Tutorial: Your First Workflow
Let's build a complete workflow to understand Duraflow's features.
What We'll Build
A simple "Order Processing" workflow that demonstrates:
- Step execution with output persistence
- Retry logic for transient failures
- Error handling
Step 1: Define the Workflow
Create workflows/order.ts:
typescript
import { workflow } from "@duraflow/sdk";
interface OrderInput {
orderId: string;
customerEmail: string;
items: Array<{ name: string; price: number }>;
}
interface OrderOutput {
orderId: string;
confirmationNumber: string;
total: number;
}
export const orderWorkflow = workflow<OrderInput, OrderOutput>(
"order-processing",
async (ctx) => {
const { orderId, customerEmail, items } = ctx.input;
// Step 1: Validate order
const validated = await ctx.step.run("validate-order", async () => {
if (!items || items.length === 0) {
throw new Error("Order has no items");
}
if (!customerEmail.includes("@")) {
throw new Error("Invalid email address");
}
return { valid: true, itemCount: items.length };
});
// Step 2: Calculate total
const total = await ctx.step.run("calculate-total", async () => {
return items.reduce((sum, item) => sum + item.price, 0);
});
// Step 3: Process payment (with retry for transient failures)
const payment = await ctx.step.run(
"process-payment",
async () => {
// Simulate API call that might fail transiently
const success = await fakePaymentApi(orderId, total);
if (!success) throw new Error("Payment failed");
return { transactionId: "TXN-" + Date.now(), amount: total };
},
{
retries: 3, // Retry up to 3 times on failure
},
);
// Step 4: Create order
const order = await ctx.step.run("create-order", async () => {
return {
orderId,
confirmationNumber:
"CONF-" + Math.random().toString(36).substr(2, 9).toUpperCase(),
total: total,
};
});
return order;
},
);
// Mock function to simulate payment API
async function fakePaymentApi(
orderId: string,
amount: number,
): Promise<boolean> {
// 30% chance of transient failure
return Math.random() > 0.3;
}Step 2: Register Workflows
Create workflows/index.ts:
typescript
export { orderWorkflow } from "./order";Step 3: Start Engine with Workflows
bash
DURAFLOW_WORKFLOWS=./workflows/index.ts npm run devStep 4: Submit Task
Using the SDK or gRPC client:
typescript
import { AgentServiceClient, credentials } from "@duraflow/proto";
const client = new AgentServiceClient(
"localhost:50051",
credentials.createInsecure(),
);
// Submit order
const response = await client.submitTask({
workflowName: "order-processing",
input: JSON.stringify({
orderId: "ORD-123",
customerEmail: "customer@example.com",
items: [
{ name: "Widget", price: 29.99 },
{ name: "Gadget", price: 49.99 },
],
}),
});
console.log("Task ID:", response.taskId);
// Poll for status
const status = await client.getTaskStatus({ taskId: response.taskId });
console.log("Status:", status.status);
console.log("Output:", status.output);What Happens Behind the Scenes
1. Task submitted → stored in agent_tasks (status: PENDING)
2. Poller picks up task → status: RUNNING
3. Step: validate-order
- Creates step_runs record
- Executes function
- Saves output to step_runs.output
- Status: COMPLETED
4. Step: calculate-total
- Same process as above
- Status: COMPLETED
5. Step: process-payment
- First attempt: might fail
- Retry with exponential backoff: 1s → 4s → 16s
- Eventually succeeds (or fails after 3 retries)
- Status: COMPLETED (or FAILED)
6. Step: create-order
- Status: COMPLETED
7. Task completed → status: COMPLETED, output storedCrash Recovery Demo
Simulate a Crash
- Start workflow
- Let it run for 2-3 steps
- Kill the engine (
Ctrl+C) - Restart the engine
What happens:
- Duraflow finds the task in RUNNING state
- Checks step_runs for completed steps
- Resumes from first incomplete step
- Completed steps are NOT re-executed (memoization)
Error Handling
If Payment Fails Permanently
typescript
// After 3 retries, the step is marked as FAILED
// Workflow fails, task status becomes FAILED
const status = await client.getTaskStatus({ taskId: response.taskId });
// status.status === "failed"
// status.error contains the error detailsCancel a Running Task
typescript
await client.cancelTask({ taskId: response.taskId });
// Task status becomes CANCELLED
// If running, steps don't execute compensation (by default)Complete Code
typescript
// Full example with error handling
import { workflow, taskStatus } from "@duraflow/sdk";
import { AgentServiceClient, credentials } from "@duraflow/proto";
const orderWorkflow = workflow("order-processing", async (ctx) => {
// ... workflow implementation ...
});
async function submitAndMonitor() {
const client = new AgentServiceClient(
"localhost:50051",
credentials.createInsecure(),
);
// Submit
const { taskId } = await client.submitTask({
workflowName: "order-processing",
input: JSON.stringify({
orderId: "ORD-123",
customerEmail: "test@example.com",
items: [{ name: "Item", price: 10 }],
}),
});
// Poll for completion
while (true) {
const status = await client.getTaskStatus({ taskId });
if (status.status === taskStatus.COMPLETED) {
console.log("Success!", status.output);
break;
} else if (status.status === taskStatus.FAILED) {
console.log("Failed:", status.error);
break;
}
await new Promise((r) => setTimeout(r, 1000));
}
}From here, Core Concepts covers the internals (SKIP LOCKED, the reaper, leader election) and Sagas covers compensation in more depth.