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:
- A Ductape account and workspace
- A product created in your workspace
- The Ductape SDK installed in your project
- 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:
- Steps with
allow_fail: true: Workflow continues - 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:
- Step Types - Learn about all available ctx methods
- Execution & Rollbacks - Understand execution flow
- Examples - See real-world patterns
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.