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.

Features

Features are durable, multi-step processes that execute a series of operations with automatic state persistence, retries, and rollbacks. They are designed for complex business processes that need to survive failures and maintain consistency.

When to Use Features

Use Features when you need:

  • Durability - Steps that must complete even if your server restarts
  • Rollbacks - Automatic compensation when something fails mid-process
  • Long-running processes - Operations that span minutes, hours, or days
  • Complex dependencies - Steps that depend on multiple previous steps
  • State checkpoints - Save progress and resume from where you left off
  • Multi-step business processes - Any operation with multiple steps that should succeed or fail together

Quick Example

Here's a simple order fulfillment feature using the code-first API:

const orderFeature = await ductape.features.define({
tag: 'order-fulfillment',
name: 'Order Fulfillment',
handler: async (ctx) => {
// Step 1: Validate the order
const validation = await ctx.step('validate', async () => {
return ctx.api.run({
app: 'orders-api',
event: 'validate-order',
input: { body: { orderId: ctx.input.orderId } },
});
});

if (!validation.valid) {
return { success: false, error: validation.reason };
}

// Step 2: Process payment (with rollback)
const payment = await ctx.step(
'process-payment',
async () => {
return ctx.api.run({
app: 'stripe',
event: 'create-charge',
input: { body: { amount: ctx.input.amount } },
});
},
// Rollback handler - refund if later steps fail
async (result) => {
await ctx.api.run({
app: 'stripe',
event: 'refund-charge',
input: { body: { chargeId: result.id } },
});
}
);

// Step 3: Reserve inventory
await ctx.step('reserve-inventory', async () => {
return ctx.database.insert({
database: 'inventory-db',
event: 'reserve-items',
data: { items: ctx.input.items, orderId: ctx.input.orderId },
});
});

// Step 4: Send confirmation email (non-critical)
await ctx.step(
'send-confirmation',
async () => {
await ctx.notification.email({
notification: 'order-notifications',
event: 'order-confirmed',
recipients: [ctx.input.email],
subject: { orderId: ctx.input.orderId },
template: { orderId: ctx.input.orderId, total: ctx.input.amount },
});
},
null, // No rollback needed
{ allow_fail: true } // Continue even if email fails
);

return { success: true, orderId: ctx.input.orderId, chargeId: payment.id };
},
});

How Features Work

Every feature has:

  1. A handler function - Contains your business logic with steps
  2. Steps - Individual operations that can be retried and rolled back
  3. Context (ctx) - Provides access to Ductape components and feature state
  4. Rollback handlers - Optional functions to undo completed work on failure
Input → ctx.step('A') → ctx.step('B') → ctx.step('C') → Output
↓ ↓ ↓
Rollback Rollback Rollback
(if C fails)

The Feature Context

The ctx object provides everything you need inside a feature:

Input & Metadata

ctx.input           // Feature input data
ctx.feature_id // Unique execution ID
ctx.feature_tag // Feature tag
ctx.env // Environment (dev, staging, prd)
ctx.product // Product tag

Ductape Components

ComponentDescription
ctx.actionCall external APIs
ctx.databaseDatabase operations
ctx.graphGraph database queries
ctx.notificationSend emails, SMS, push
ctx.storageFile upload/download
ctx.publishMessage broker publishing
ctx.quotaRate limiting
ctx.fallbackHandle failures with alternatives

Control Flow

MethodDescription
ctx.step()Define a step with optional rollback
ctx.sleep()Pause execution
ctx.waitForSignal()Wait for external event
ctx.checkpoint()Save state for recovery
ctx.triggerRollback()Manually trigger rollback

State Management

ctx.setState('key', value);     // Save state
ctx.getState('key'); // Retrieve state
ctx.state // All state
ctx.steps // Step results
ctx.completed_steps // List of completed step tags

Defining Steps

Steps are defined with ctx.step():

const result = await ctx.step(
'step-tag', // Unique step identifier
handler, // Async function that does the work
rollback?, // Optional rollback function
options? // Optional step options
);

Basic Step

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 },
});
});

Step with Rollback

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

Step with Options

await ctx.step(
'send-analytics',
async () => {
await ctx.api.run({ app: 'analytics', event: 'track', input: {} });
},
null, // No rollback
{
allow_fail: true, // Continue on failure
retries: 3, // Retry count
timeout: 5000, // Timeout in ms
}
);

Component Usage

Actions (API Calls)

const result = await ctx.api.run({
app: 'stripe',
event: 'create-charge',
input: {
body: { amount: 1000, currency: 'usd' },
headers: { 'Idempotency-Key': ctx.input.orderId },
},
retries: 3,
timeout: 10000,
});

Database Operations

// Insert
const user = await ctx.database.insert({
database: 'users-db',
event: 'create-user',
data: { email: ctx.input.email },
});

// Query
const orders = await ctx.database.query({
database: 'orders-db',
event: 'find-orders',
params: { customerId: ctx.input.customerId },
});

// Update
await ctx.database.update({
database: 'users-db',
event: 'update-user',
where: { id: user.id },
data: { status: 'active' },
});

// Delete
await ctx.database.delete({
database: 'orders-db',
event: 'delete-order',
where: { id: ctx.input.orderId },
});

Graph Database

// Create node
const userNode = await ctx.graph.createNode({
graph: 'social-graph',
labels: ['User'],
properties: { name: ctx.input.name },
});

// Create relationship
await ctx.graph.createRelationship({
graph: 'social-graph',
from: userNode.id,
to: ctx.input.friendId,
type: 'FRIENDS_WITH',
});

// Query
const friends = await ctx.graph.query({
graph: 'social-graph',
action: 'find-friends',
params: { userId: ctx.input.userId },
});

Notifications

// Email
await ctx.notification.email({
notification: 'transactional',
event: 'welcome-email',
recipients: [ctx.input.email],
subject: { name: ctx.input.name },
template: { name: ctx.input.name, link: ctx.input.activationLink },
});

// SMS
await ctx.notification.sms({
notification: 'alerts',
event: 'verification-code',
phones: [ctx.input.phone],
message: { code: ctx.input.verificationCode },
});

// Push
await ctx.notification.push({
notification: 'mobile',
event: 'order-update',
tokens: [ctx.input.deviceToken],
title: { status: 'Shipped' },
body: { orderId: ctx.input.orderId },
data: { orderId: ctx.input.orderId },
});

Storage

// Upload
const file = await ctx.storage.upload({
storage: 'documents',
event: 'upload-receipt',
input: {
buffer: ctx.input.fileData,
fileName: `receipt-${ctx.input.orderId}.pdf`,
mimeType: 'application/pdf',
},
});

// Download
const download = await ctx.storage.download({
storage: 'templates',
event: 'get-template',
input: { file_key: 'invoice-template.pdf' },
});

// Delete
await ctx.storage.delete({
storage: 'temp-files',
event: 'cleanup',
input: { file_key: ctx.input.tempFileKey },
});

Message Publishing

await ctx.messaging.produce({
event: 'order-events:new-order',
message: {
orderId: ctx.input.orderId,
items: ctx.input.items,
},
});

Control Flow

Sleep

Pause feature execution:

await ctx.sleep(5000);        // 5 seconds
await ctx.sleep('5m'); // 5 minutes
await ctx.sleep('1h'); // 1 hour

Wait for Signal

Pause until an external event:

const approval = await ctx.waitForSignal('order-approved', {
timeout: '24h',
});

console.log('Approved by:', approval.approvedBy);

Send a signal from outside:

await ductape.features.signal({
feature_id: 'wf-123',
signal: 'order-approved',
payload: { approvedBy: 'manager@company.com' },
});

Checkpoint

Save state for recovery:

await ctx.checkpoint('payment-complete', {
chargeId: payment.id,
timestamp: Date.now(),
});

Manual Rollback

Trigger rollback programmatically:

if (fraudCheck.risk > 0.8) {
await ctx.triggerRollback('High fraud risk detected');
}

Executing Features

const result = await ductape.features.execute({
tag: 'order-fulfillment',
input: {
orderId: 'ORD-12345',
amount: 99.99,
email: 'customer@example.com',
items: ['SKU-001', 'SKU-002'],
},
});

console.log('Status:', result.status); // 'completed' | 'failed' | 'rolled_back'
console.log('Output:', result.output);
console.log('Duration:', result.execution_time, 'ms');
console.log('Completed Steps:', result.completed_steps);

Portable control flow

Stored Features execute their compiled step graph; the handler is not rerun at runtime. Use ctx.branch with ctx.when for runtime decisions, ctx.each with ctx.sampleInput for fixed graph expansion, and portable Functions for runtime-sized collection algorithms. Do not apply native if/else, ??, loops, or array methods to unresolved ctx.input or step-result proxies.

Validate the entire project before syncing. Validation is read-only, and sync runs the same fail-closed preflight before any registration code starts:

ductape features validate --json
ductape features sync

See Building Features - Portable control flow.

Next Steps

See Also

  • Jobs - Schedule features to run automatically