DevelopmentWorkflows
Creating Workflows
Creating Workflows
This guide walks you through creating a new workflow in the SuitsBooks system.
File Structure
workflows/
├── workflows.ts
├── registry.ts
├── instance.ts
└── worker.ts
Overview
Workflows are defined in workflows/workflows.ts using the defineWorkflows() function. Each workflow consists of:
- Input Interface: TypeScript interface defining the input parameters
- Workflow Definition: The workflow logic with steps
- Registry Entry: Registration in the workflow registry
Step-by-Step Guide
Step 1: Define Input Interface
Create a TypeScript interface for your workflow inputs:
interface MyWorkflowInput {
userId: string;
companyId: string;
// ... other required fields
}
```typescript
### Step 2: Add Workflow to `workflows/workflows.ts`
Add your workflow definition inside the `defineWorkflows()` function:
```typescript
export function defineWorkflows(ow: OpenWorkflow) {
// ... existing workflows ...
const myNewWorkflow = ow.defineWorkflow(
{ name: "my-new-workflow" },
async ({ input, step }: { input: MyWorkflowInput; step: any }) => {
// Step 1: Fetch data
const data = await step.run({ name: "fetch-data" }, async () => {
// Your logic here
return { someData: "value" };
});
// Step 2: Process data
const result = await step.run({ name: "process-data" }, async () => {
// Your logic here, can use data from previous step
return { processed: data.someData };
});
// Step 3: Send notification
await step.run({ name: "send-notification" }, async () => {
// Your logic here
});
return { result };
},
);
return {
// ... existing workflows ...
myNewWorkflow,
};
}
```typescript
### Step 3: Register Workflow in `workflows/registry.ts`
Add your workflow to the registry:
```typescript
const workflows: Record<string, any> = {
"send-welcome-email": workflowsCache.sendWelcomeEmail,
"send-email-received-ein": workflowsCache.sendEmailReceivedEin,
"my-new-workflow": workflowsCache.myNewWorkflow, // Add this line
};
```typescript
### Step 4: Add API Parameter Mapping (Optional)
If your workflow needs parameter mapping (snake_case ↔ camelCase), update the API route:
```typescript
// In apps/dashboard/app/api/workflow/[workflowName]/route.ts
if (workflowName === "my-new-workflow") {
workflowInput = {
userId: body.userId || body.user_id,
companyId: body.companyId || body.company_id,
// ... map other parameters
};
}
```typescript
## Workflow Structure
### Basic Workflow
```typescript
const simpleWorkflow = ow.defineWorkflow(
{ name: "simple-workflow" },
async ({ input, step }) => {
const result = await step.run(
{ name: "do-something" },
async () => {
// Your business logic
return { success: true };
}
);
return result;
}
);
```typescript
### Multi-Step Workflow
```typescript
const multiStepWorkflow = ow.defineWorkflow(
{ name: "multi-step-workflow" },
async ({ input, step }) => {
// Step 1: Fetch user
const user = await step.run({ name: "fetch-user" }, async () => {
const result = await db
.select()
.from(schema.users)
.where(eq(schema.users.userId, input.userId))
.limit(1);
return result[0];
});
// Step 2: Process user data
const processed = await step.run({ name: "process-user" }, async () => {
// Can access user from previous step
return {
fullName: `${user.firstName} ${user.lastName}`,
email: user.email,
};
});
// Step 3: Send email
await step.run({ name: "send-email" }, async () => {
return await email({
userId: input.userId,
recipients: [processed.email],
// ...
});
});
return { user, processed };
}
);
```typescript
## Step Best Practices
### 1. Use Descriptive Step Names
```typescript
// Good
await step.run({ name: "fetch-user-from-database" }, async () => { ... });
// Bad
await step.run({ name: "step1" }, async () => { ... });
```typescript
### 2. Keep Steps Focused
Each step should do one thing:
```typescript
// Good: Each step has a single responsibility
await step.run({ name: "validate-input" }, async () => { ... });
await step.run({ name: "fetch-data" }, async () => { ... });
await step.run({ name: "transform-data" }, async () => { ... });
// Bad: One step doing everything
await step.run({ name: "process" }, async () => {
// validation
// fetching
// transformation
});
```typescript
### 3. Make Steps Idempotent
Steps should be safe to retry:
```typescript
// Good: Idempotent - safe to retry
await step.run({ name: "update-status" }, async () => {
await db
.update(schema.items)
.set({ status: "processed" })
.where(eq(schema.items.id, input.itemId));
// Same result every time
});
// Bad: Not idempotent - creates duplicates on retry
await step.run({ name: "create-log" }, async () => {
await db.insert(schema.logs).values({ message: "processed" });
// Creates duplicate on retry
});
```typescript
### 4. Handle Errors Appropriately
```typescript
await step.run({ name: "send-email" }, async () => {
try {
return await email({ ... });
} catch (error) {
// Log error details
console.error("Failed to send email:", error);
// Re-throw to mark step as failed
throw error;
}
});
```typescript
### 5. Return Useful Data
Return data that subsequent steps might need:
```typescript
const user = await step.run({ name: "fetch-user" }, async () => {
const result = await db.select()...;
return result[0]; // Return the user object
});
// Later steps can use the user data
await step.run({ name: "send-email" }, async () => {
return await email({
recipientName: user.fullName, // Using data from previous step
recipients: [user.email],
});
});
```typescript
## Common Patterns
### Database Operations
```typescript
const db = drizzle(process.env.DATABASE_URL!);
// Fetch
const user = await step.run({ name: "fetch-user" }, async () => {
const result = await db
.select()
.from(schema.users)
.where(eq(schema.users.userId, input.userId))
.limit(1);
return result[0];
});
// Update
await step.run({ name: "update-status" }, async () => {
await db
.update(schema.users)
.set({ status: "active" })
.where(eq(schema.users.userId, input.userId));
});
// Insert
await step.run({ name: "create-record" }, async () => {
const [record] = await db
.insert(schema.records)
.values({ ... })
.returning();
return record;
});
```typescript
### API Calls
```typescript
await step.run({ name: "call-external-api" }, async () => {
const response = await fetch("https://api.example.com/endpoint", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`,
},
body: JSON.stringify({ ... }),
});
if (!response.ok) {
throw new Error(`API call failed: ${response.statusText}`);
}
return await response.json();
});
```typescript
### Email Sending
```typescript
import { email } from "./utils/send-mail";
await step.run({ name: "send-email" }, async () => {
return await email({
userId: input.userId,
companyId: input.companyId,
organizationId: input.organizationId,
emailAction: "my_email_template",
recipientName: "John Doe",
recipients: ["john@example.com"],
resource_id_ref: input.resourceId,
ccs: input.ccs || [],
});
});
```typescript
## Testing Workflows
### Manual Testing
1. Start the worker:
```bash
tsx workflows/worker.ts
```typescript
2. Trigger workflow via API:
```bash
curl -X POST http://localhost:3000/api/workflow/my-new-workflow \
-H "Content-Type: application/json" \
-d '{"userId": "test-user"}'
```typescript
3. Check status:
```bash
curl "http://localhost:3000/api/workflow/my-new-workflow?runId=run-123"
```typescript
### Unit Testing
Test individual step logic:
```typescript
describe("My Workflow", () => {
it("should process data correctly", async () => {
const result = await processData({ ... });
expect(result).toEqual({ ... });
});
});
```typescript
## Error Handling
### Step-Level Errors
Errors in steps are automatically caught and stored:
```typescript
await step.run({ name: "risky-operation" }, async () => {
// If this throws, the step is marked as failed
// The error is stored in the database
// The workflow can retry or fail based on configuration
if (someCondition) {
throw new Error("Something went wrong");
}
});
```typescript
### Workflow-Level Errors
Handle errors at the workflow level:
```typescript
const myWorkflow = ow.defineWorkflow(
{ name: "my-workflow" },
async ({ input, step }) => {
try {
const result = await step.run({ name: "do-work" }, async () => {
// ...
});
return { success: true, result };
} catch (error) {
// Log error
console.error("Workflow failed:", error);
// Return error information
return { success: false, error: error.message };
}
}
);
```typescript
## Advanced Features
### Conditional Steps
```typescript
if (input.shouldSendEmail) {
await step.run({ name: "send-email" }, async () => {
// ...
});
}
```typescript
### Parallel Steps (Future)
Currently, steps run sequentially. For parallel execution, use separate workflows or implement within a single step.
### Child Workflows
Workflows can trigger other workflows (future feature).
## Checklist
When creating a new workflow:
- [ ] Define input interface
- [ ] Add workflow to `defineWorkflows()`
- [ ] Export workflow in return object
- [ ] Register in `workflows/registry.ts`
- [ ] Add API parameter mapping (if needed)
- [ ] Test workflow execution
- [ ] Document workflow in README
- [ ] Add error handling
- [ ] Make steps idempotent
## Examples
See [Examples](./examples.md) for complete workflow examples.