XYLEX Group
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:

  1. Input Interface: TypeScript interface defining the input parameters
  2. Workflow Definition: The workflow logic with steps
  3. 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.