Getting Started with Features
Feature handlers are compiled into portable JSON. Every ctx.step() callback must record a Ductape
primitive or a portable function. Ductape does not serialize arbitrary
JavaScript closures and never uses a recording-time return value as runtime output.
This guide walks you through creating and running your first feature 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
- TypeScript
- NestJS
npm install @ductape/sdk@0.1.8
npm install @ductape/nestjs@0.1.0 @ductape/sdk@0.1.8
Step 2: Initialize the SDK
- TypeScript
import Ductape from '@ductape/sdk';
const ductape = new Ductape({
accessKey: 'your-access-key',
});
See SDK runtime defaults — product and env belong on the constructor only.
Step 3: Define Your First Feature
Use ductape.features.define() to create a feature with a handler function:
- TypeScript
const onboardingFeature = await ductape.features.define({
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 feature output
return {
success: true,
userId: user.id,
};
},
});
Step 4: Execute the Feature
- TypeScript
const result = await ductape.features.execute({
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:
- TypeScript
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 feature
await ductape.features.define({
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 feature
const result = await ductape.features.execute({
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:
- TypeScript
handler: async (ctx) => {
// Input data
ctx.input.email // Access input fields
ctx.input.name
// Feature metadata
ctx.feature_id // Unique execution ID
ctx.feature_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:
- TypeScript
{
status: 'completed', // 'completed', 'failed', or 'rolled_back'
feature_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: Feature continues - Steps without
allow_fail: Feature stops and rolls back
For example, if send-welcome fails (and has allow_fail: true), the feature continues. If create-user fails, the feature fails immediately.
Adding Rollback Logic
To undo completed steps when something fails:
- TypeScript
const payment = await ctx.step(
'charge-payment',
// Main handler
async () => {
return ctx.api.run({
app: 'stripe',
event: 'create-charge',
input: { body: { amount: ctx.input.amount } },
});
},
// Rollback handler - called if a later step fails
async (result) => {
await ctx.api.run({
app: 'stripe',
event: 'refund-charge',
input: { body: { chargeId: result.id } },
});
}
);
When the feature rolls back, it calls each rollback handler with the original step's result.
Next Steps
Now that you've created your first feature:
- Step Types - Learn about all available ctx methods
- Execution & Rollbacks - Understand execution flow
- Examples - See real-world patterns
Troubleshooting
"Feature not found"
Make sure you've defined the feature 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.