Workflows are the backbone of any serious application. Whether you’re processing payments, onboarding users, or orchestrating complex business logic, you need a reliable way to coordinate multiple steps, handle failures, and ensure everything happens in the right order.Traditional workflow engines like Temporal and Inngest are brilliant at this, but they come with a lot of overhead. Sailhouse takes a different approach - we’ve built workflow capabilities right into our event-driven architecture, giving you the power of complex orchestration without the complexity.
Most workflow engines require you to define your entire workflow upfront in a rigid structure. With Sailhouse, workflows emerge naturally from your event-driven architecture using primitives you already understand: topics, subscriptions, wait groups, and cron jobs.This means:
Incredible flexibility - workflows adapt automatically based on your logic. Send 1 event or 4 depending on conditions like user preferences or feature flags
Natural debugging - each step is a separate event you can inspect and replay
Built-in observability - every event is tracked and can be monitored
It’s just events - it’s no different to the rest of your application
The simplest workflows on Sailhouse are just a chain of events, but sometimes, you need a little more control and parallelism - so let’s dive into how you can achieve that.
Let’s walk through a real-world example: processing a new user signup. This workflow needs to:
Validate the user’s email
Create their account in the database
Send a welcome email
Set up their default preferences
Trigger a follow-up email sequence
Here’s how you’d build this with Sailhouse:
Copy
// Step 1: Start the workflowawait client.publish("user-signup-started", { email: "jane@example.com", name: "Jane Doe", signup_source: "homepage"});// Step 2: Use a wait group to trigger parallel tasks, and send an event// to `user-onboarding-complete` when all tasks are completedawait client.wait("user-onboarding-complete", [ { topic: "validate-email", body: { email: "jane@example.com" } }, { topic: "create-account", body: { email: "jane@example.com", name: "Jane Doe" } }, { topic: "send-welcome-email", body: { email: "jane@example.com", name: "Jane Doe" } }, { topic: "setup-preferences", body: { email: "jane@example.com" } }]);
Each of these topics has its own subscription handling the specific logic. When all four steps complete, the user-onboarding-complete event fires, which can trigger the follow-up sequence.
Real workflows aren’t linear. Sometimes you need different paths based on conditions, error handling, or user actions. Sailhouse handles this naturally through conditional event publishing.Let’s extend our signup example to handle different user types:
Copy
client.subscribe("create-account", async (event) => { const { email, name, signup_source } = event.body; const user = await createUser(email, name); // Branch based on signup source if (signup_source === "enterprise_trial") { await client.publish("setup-enterprise-trial", { user_id: user.id, email: email }); } else if (signup_source === "referral") { await client.publish("process-referral-bonus", { user_id: user.id, referrer_email: event.body.referrer_email }); } // Always send to the general setup flow await client.publish("setup-standard-account", { user_id: user.id, email: email });});
One of the biggest advantages of Sailhouse workflows is built-in error handling. Since each step is an event, you get automatic retries, dead letter queues, and graceful failure handling without any extra configuration.
Some workflows span days, weeks, or even months. Think about a user onboarding sequence, subscription renewals, or compliance workflows. Sailhouse handles this brilliantly with scheduled events and cron jobs.
Many business processes require human approval. Traditional workflow engines make this cumbersome, but Sailhouse’s event-driven approach makes it natural.
Copy
client.subscribe("expense-submitted", async (event) => { const { expense_id, amount } = event.body; // Auto-approve small expenses if (amount < 100) { await client.publish("expense-approved", { expense_id, approved_by: "auto-approval", approved_at: new Date() }); return; } // Send for manual approval await client.publish("approval-required", { expense_id, amount, manager_email: await getManagerEmail(event.body.employee_id) }); // Set a timeout for auto-escalation const fortyEightHoursFromNow = new Date(Date.now() + 48 * 60 * 60 * 1000); await client.publish("escalate-approval", { expense_id }, { date: fortyEightHoursFromNow });});// Handle the approval response (from your web app)client.subscribe("approval-response", async (event) => { const { expense_id, approved, approver_id } = event.body; if (approved) { await client.publish("expense-approved", { expense_id, approved_by: approver_id, approved_at: new Date() }); } else { await client.publish("expense-rejected", { expense_id, rejected_by: approver_id, rejected_at: new Date() }); }});
If you’re running a microservices architecture, Sailhouse workflows excel at coordinating cross-service operations while maintaining service boundaries.
Copy
// Order processing workflow across multiple servicesawait client.wait("order-processing-complete", [ { topic: "validate-inventory", // Handled by inventory service body: { order_id: "ord_123", items: [...] } }, { topic: "process-payment", // Handled by payment service body: { order_id: "ord_123", amount: 99.99 } }, { topic: "reserve-shipping", // Handled by logistics service body: { order_id: "ord_123", address: {...} } }]);// When all services complete, fulfill the orderclient.subscribe("order-processing-complete", async (event) => { await client.publish("fulfill-order", { order_id: event.body.order_id });});
Each service only needs to know about its own topics, maintaining clean boundaries while participating in the larger workflow.