Skip to main content
Preview
Preview Feature — This feature is currently in preview and under active development. APIs and functionality may change. We recommend testing thoroughly before using in production.

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({
accessKey: 'your-access-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.