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 Features

This guide walks you through creating and running your first feature 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@0.1.8

Step 2: Initialize the SDK

import Ductape from '@ductape/sdk';

const ductape = new Ductape({
accessKey: 'your-access-key',
});

See SDK runtime defaultsproduct 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:

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

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:

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:

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:

{
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:

  1. Steps with allow_fail: true: Feature continues
  2. 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:

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:

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.