Skip to main content

Getting Started with Workflows

This guide walks you through creating and running your first workflow using the code-first API.

Prerequisites

Before you begin, make sure you have:

  1. A Ductape account and workspace
  2. A product created in your workspace
  3. The Ductape SDK installed in your project
  4. At least one connected app or database

Step 1: Install the SDK

npm install @ductape/sdk

Step 2: Initialize the SDK

import Ductape from '@ductape/sdk';

const ductape = new Ductape({
user_id: 'your-user-id',
workspace_id: 'your-workspace-id',
private_key: 'your-private-key',
});

Step 3: Define Your First Workflow

Use ductape.workflows.define() to create a workflow with a handler function:

const onboardingWorkflow = await ductape.workflows.define({
product: 'my-product',
tag: 'user-onboarding',
name: 'User Onboarding',
description: 'Onboards new users with welcome email and audit log',

handler: async (ctx) => {
// Step 1: Create user in database
const user = await ctx.step('create-user', async () => {
return ctx.database.insert({
database: 'users-db',
event: 'create-user',
data: {
email: ctx.input.email,
name: ctx.input.name,
created_at: new Date().toISOString(),
},
});
});

// Step 2: Send welcome email
await ctx.step(
'send-welcome',
async () => {
await ctx.notification.email({
notification: 'transactional',
event: 'welcome-email',
recipients: [ctx.input.email],
subject: { name: ctx.input.name },
template: { name: ctx.input.name },
});
},
null, // No rollback needed for email
{ allow_fail: true } // Continue even if email fails
);

// Step 3: Create audit log
await ctx.step('audit-log', async () => {
return ctx.database.insert({
database: 'audit-db',
event: 'create-log',
data: {
action: 'user_created',
user_id: user.id,
timestamp: new Date().toISOString(),
},
});
});

// Return workflow output
return {
success: true,
userId: user.id,
};
},
});

Step 4: Execute the Workflow

const result = await ductape.workflows.execute({
product: 'my-product',
env: 'dev',
tag: 'user-onboarding',
input: {
email: 'john@example.com',
name: 'John Doe',
},
});

if (result.status === 'completed') {
console.log('User onboarded successfully!');
console.log('User ID:', result.output.userId);
} else {
console.error('Onboarding failed:', result.error);
}

Complete Example

Here's everything together:

import Ductape from '@ductape/sdk';

async function main() {
// Initialize SDK
const ductape = new Ductape({
user_id: 'your-user-id',
workspace_id: 'your-workspace-id',
private_key: 'your-private-key',
});

// Define the workflow
await ductape.workflows.define({
product: 'my-product',
tag: 'user-onboarding',
name: 'User Onboarding',

handler: async (ctx) => {
// Create user
const user = await ctx.step('create-user', async () => {
return ctx.database.insert({
database: 'users-db',
event: 'create-user',
data: {
email: ctx.input.email,
name: ctx.input.name,
},
});
});

// Send welcome email (non-critical)
await ctx.step(
'send-welcome',
async () => {
await ctx.notification.email({
notification: 'transactional',
event: 'welcome-email',
recipients: [ctx.input.email],
template: { name: ctx.input.name },
});
},
null,
{ allow_fail: true }
);

return { userId: user.id };
},
});

// Execute the workflow
const result = await ductape.workflows.execute({
product: 'my-product',
env: 'dev',
tag: 'user-onboarding',
input: {
email: 'jane@example.com',
name: 'Jane Doe',
},
});

console.log('Result:', result);
}

main().catch(console.error);

Understanding the Context

The ctx object passed to your handler provides:

handler: async (ctx) => {
// Input data
ctx.input.email // Access input fields
ctx.input.name

// Workflow metadata
ctx.workflow_id // Unique execution ID
ctx.workflow_tag // 'user-onboarding'
ctx.env // 'dev', 'staging', 'prd'
ctx.product // 'my-product'

// Ductape components
ctx.action // API calls
ctx.database // Database operations
ctx.notification // Emails, SMS, push
ctx.storage // File uploads/downloads
ctx.graph // Graph database
ctx.publish // Message brokers

// Control flow
ctx.step() // Define a step
ctx.sleep() // Pause execution
ctx.checkpoint() // Save state

// State management
ctx.setState() // Save custom state
ctx.getState() // Retrieve state

// Logging
ctx.log.info() // Log messages
ctx.log.error()
}

Understanding the Result

The execution result contains:

{
status: 'completed', // 'completed', 'failed', or 'rolled_back'
workflow_id: 'wf_abc123', // Unique execution ID
execution_time: 1250, // Total time in milliseconds
output: { // Your handler's return value
userId: 'usr_123'
},
completed_steps: [ // Steps that completed
'create-user',
'send-welcome',
'audit-log'
],
failed_step: null, // Step that failed (if any)
error: null // Error message if failed
}

What Happens on Failure?

If a step fails:

  1. Steps with allow_fail: true: Workflow continues
  2. Steps without allow_fail: Workflow stops and rolls back

For example, if send-welcome fails (and has allow_fail: true), the workflow continues. If create-user fails, the workflow fails immediately.

Adding Rollback Logic

To undo completed steps when something fails:

const payment = await ctx.step(
'charge-payment',
// Main handler
async () => {
return ctx.action.run({
app: 'stripe',
event: 'create-charge',
input: { body: { amount: ctx.input.amount } },
});
},
// Rollback handler - called if a later step fails
async (result) => {
await ctx.action.run({
app: 'stripe',
event: 'refund-charge',
input: { body: { chargeId: result.id } },
});
}
);

When the workflow rolls back, it calls each rollback handler with the original step's result.

Next Steps

Now that you've created your first workflow:

Troubleshooting

"Workflow not found"

Make sure you've defined the workflow with define() before executing.

"Step failed"

Check the result.error and result.failed_step for details.

"Input field not found"

Verify your input object contains all fields accessed via ctx.input.