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:
- TypeScript
- Java
- Go
- .NET
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 };
},
});
Map<String, Object> orderFeature = ductape.features.define(Map.of(
"tag", "order-fulfillment",
"name", "Order Fulfillment",
handler: async (ctx) => Map.of(
// Step 1: Validate the order
Map<String, Object> validation = ctx.step('validate', async () => Map.of(
return ctx.api.run(Map.of(
"app", "orders-api",
"event", "validate-order",
input: Map.of( body: Map.of( orderId: ctx.input.orderId ) )
));
));
if (!validation.valid) Map.of(
return Map.of( "success", false, error: validation.reason );
)
// Step 2: Process payment (with rollback)
Map<String, Object> payment = ctx.step(
'process-payment',
async () => Map.of(
return ctx.api.run(Map.of(
"app", "stripe",
"event", "create-charge",
input: Map.of( body: Map.of( amount: ctx.input.amount ) )
));
),
// Rollback handler - refund if later steps fail
async (result) => Map.of(
ctx.api.run(Map.of(
"app", "stripe",
"event", "refund-charge",
input: Map.of( body: Map.of( chargeId: result.id ) )
));
)
);
// Step 3: Reserve inventory
ctx.step('reserve-inventory', async () => Map.of(
return ctx.database.insert(Map.of(
"database", "inventory-db",
"event", "reserve-items",
data: Map.of( items: ctx.input.items, orderId: ctx.input.orderId )
));
));
// Step 4: Send confirmation email (non-critical)
ctx.step(
'send-confirmation',
async () => Map.of(
ctx.notification.email(Map.of(
"notification", "order-notifications",
"event", "order-confirmed",
recipients: [ctx.input.email],
subject: Map.of( orderId: ctx.input.orderId ),
template: Map.of( orderId: ctx.input.orderId, total: ctx.input.amount )
));
),
null, // No rollback needed
Map.of( "allow_fail", true ) // Continue even if email fails
);
return Map.of( "success", true, orderId: ctx.input.orderId, chargeId: payment.id );
)
));
orderFeature := client.features.define({
"tag": "order-fulfillment",
"name": "Order Fulfillment",
handler: async (ctx) => {
// Step 1: Validate the order
validation := 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)
payment := 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) => {
ctx.api.run({
"app": "stripe",
"event": "refund-charge",
input: { body: { chargeId: result.id } },
});
}
);
// Step 3: Reserve inventory
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)
ctx.step(
'send-confirmation',
async () => {
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 };
},
});
var orderFeature = await ductape.features.define({
["tag"] = "order-fulfillment",
["name"] = "Order Fulfillment",
handler: async (ctx) => {
// Step 1: Validate the order
var 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)
var 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:
- A handler function - Contains your business logic with steps
- Steps - Individual operations that can be retried and rolled back
- Context (ctx) - Provides access to Ductape components and feature state
- 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
- TypeScript
- Java
- Go
- .NET
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
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
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
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
| Component | Description |
|---|---|
ctx.action | Call external APIs |
ctx.database | Database operations |
ctx.graph | Graph database queries |
ctx.notification | Send emails, SMS, push |
ctx.storage | File upload/download |
ctx.publish | Message broker publishing |
ctx.quota | Rate limiting |
ctx.fallback | Handle failures with alternatives |
Control Flow
| Method | Description |
|---|---|
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
- TypeScript
- Java
- Go
- .NET
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
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
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
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():
- TypeScript
- Java
- Go
- .NET
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
);
Map<String, Object> result = ctx.step(
'step-tag', // Unique step identifier
handler, // Async function that does the work
rollback?, // Optional rollback function
options? // Optional step options
);
result := ctx.step(
'step-tag', // Unique step identifier
handler, // Async function that does the work
rollback?, // Optional rollback function
options? // Optional step options
);
var 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
- TypeScript
- Java
- Go
- .NET
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 },
});
});
Map<String, Object> user = ctx.step('create-user', async () => Map.of(
return ctx.database.insert(Map.of(
"database", "users-db",
"event", "create-user",
data: Map.of( email: ctx.input.email, name: ctx.input.name )
));
));
user := ctx.step('create-user', async () => {
return ctx.database.insert({
"database": "users-db",
"event": "create-user",
data: { email: ctx.input.email, name: ctx.input.name },
});
});
var 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
- TypeScript
- Java
- Go
- .NET
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 } },
});
}
);
Map<String, Object> charge = ctx.step(
'charge-card',
async () => Map.of(
return ctx.api.run(Map.of(
"app", "stripe",
"event", "create-charge",
input: Map.of( body: Map.of( amount: ctx.input.amount ) )
));
),
async (result) => Map.of(
// Called if a later step fails
ctx.api.run(Map.of(
"app", "stripe",
"event", "refund",
input: Map.of( body: Map.of( chargeId: result.id ) )
));
)
);
charge := 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
ctx.api.run({
"app": "stripe",
"event": "refund",
input: { body: { chargeId: result.id } },
});
}
);
var 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
- TypeScript
- Java
- Go
- .NET
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
}
);
ctx.step(
'send-analytics',
async () => Map.of(
ctx.api.run(Map.of( "app", "analytics", "event", "track", input: Map.of() ));
),
null, // No rollback
Map.of(
"allow_fail", true, // Continue on failure
"retries", 3, // Retry count
"timeout", 5000, // Timeout in ms
)
);
ctx.step(
'send-analytics',
async () => {
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
}
);
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)
- TypeScript
- Java
- Go
- .NET
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,
});
Map<String, Object> result = ctx.api.run(Map.of(
"app", "stripe",
"event", "create-charge",
input: Map.of(
body: Map.of( "amount", 1000, "currency", "usd" ),
headers: Map.of( 'Idempotency-Key': ctx.input.orderId )
),
"retries", 3,
"timeout", 10000
));
result := ctx.api.run({
"app": "stripe",
"event": "create-charge",
input: {
body: { "amount": 1000, "currency": "usd" },
headers: { 'Idempotency-Key': ctx.input.orderId },
},
"retries": 3,
"timeout": 10000,
});
var 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
- TypeScript
- Java
- Go
- .NET
// 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 },
});
// Insert
Map<String, Object> user = ctx.database.insert(Map.of(
"database", "users-db",
"event", "create-user",
data: Map.of( email: ctx.input.email )
));
// Query
Map<String, Object> orders = ctx.database.query(Map.of(
"database", "orders-db",
"event", "find-orders",
params: Map.of( customerId: ctx.input.customerId )
));
// Update
ctx.database.update(Map.of(
"database", "users-db",
"event", "update-user",
where: Map.of( id: user.id ),
data: Map.of( "status", "active" )
));
// Delete
ctx.database.delete(Map.of(
"database", "orders-db",
"event", "delete-order",
where: Map.of( id: ctx.input.orderId )
));
// Insert
user := ctx.database.insert({
"database": "users-db",
"event": "create-user",
data: { email: ctx.input.email },
});
// Query
orders := ctx.database.query({
"database": "orders-db",
"event": "find-orders",
params: { customerId: ctx.input.customerId },
});
// Update
ctx.database.update({
"database": "users-db",
"event": "update-user",
where: { id: user.id },
data: { "status": "active" },
});
// Delete
ctx.database.delete({
"database": "orders-db",
"event": "delete-order",
where: { id: ctx.input.orderId },
});
// Insert
var user = await ctx.database.insert({
["database"] = "users-db",
["event"] = "create-user",
data: { email: ctx.input.email },
});
// Query
var 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
- TypeScript
- Java
- Go
- .NET
// 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 },
});
// Create node
Map<String, Object> userNode = ctx.graph.createNode(Map.of(
"graph", "social-graph",
labels: ['User'],
properties: Map.of( name: ctx.input.name )
));
// Create relationship
ctx.graph.createRelationship(Map.of(
"graph", "social-graph",
from: userNode.id,
to: ctx.input.friendId,
"type", "FRIENDS_WITH"
));
// Query
Map<String, Object> friends = ctx.graph.query(Map.of(
"graph", "social-graph",
"action", "find-friends",
params: Map.of( userId: ctx.input.userId )
));
// Create node
userNode := ctx.graph.createNode({
"graph": "social-graph",
labels: ['User'],
properties: { name: ctx.input.name },
});
// Create relationship
ctx.graph.createRelationship({
"graph": "social-graph",
from: userNode.id,
to: ctx.input.friendId,
"type": "FRIENDS_WITH",
});
// Query
friends := ctx.graph.query({
"graph": "social-graph",
"action": "find-friends",
params: { userId: ctx.input.userId },
});
// Create node
var 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
var friends = await ctx.graph.query({
["graph"] = "social-graph",
["action"] = "find-friends",
params: { userId: ctx.input.userId },
});
Notifications
- TypeScript
- Java
- Go
- .NET
// 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 },
});
// Email
ctx.notification.email(Map.of(
"notification", "transactional",
"event", "welcome-email",
recipients: [ctx.input.email],
subject: Map.of( name: ctx.input.name ),
template: Map.of( name: ctx.input.name, link: ctx.input.activationLink )
));
// SMS
ctx.notification.sms(Map.of(
"notification", "alerts",
"event", "verification-code",
phones: [ctx.input.phone],
message: Map.of( code: ctx.input.verificationCode )
));
// Push
ctx.notification.push(Map.of(
"notification", "mobile",
"event", "order-update",
tokens: [ctx.input.deviceToken],
title: Map.of( "status", "Shipped" ),
body: Map.of( orderId: ctx.input.orderId ),
data: Map.of( orderId: ctx.input.orderId )
));
// Email
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
ctx.notification.sms({
"notification": "alerts",
"event": "verification-code",
phones: [ctx.input.phone],
message: { code: ctx.input.verificationCode },
});
// Push
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 },
});
// 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
- TypeScript
- Java
- Go
- .NET
// 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 },
});
// Upload
Map<String, Object> file = ctx.storage.upload(Map.of(
"storage", "documents",
"event", "upload-receipt",
input: Map.of(
buffer: ctx.input.fileData,
fileName: `receipt-$Map.of(ctx.input.orderId).pdf`,
"mimeType", "application/pdf"
)
));
// Download
Map<String, Object> download = ctx.storage.download(Map.of(
"storage", "templates",
"event", "get-template",
input: Map.of( "file_key", "invoice-template.pdf" )
));
// Delete
ctx.storage.delete(Map.of(
"storage", "temp-files",
"event", "cleanup",
input: Map.of( file_key: ctx.input.tempFileKey )
));
// Upload
file := ctx.storage.upload({
"storage": "documents",
"event": "upload-receipt",
input: {
buffer: ctx.input.fileData,
fileName: `receipt-${ctx.input.orderId}.pdf`,
"mimeType": "application/pdf",
},
});
// Download
download := ctx.storage.download({
"storage": "templates",
"event": "get-template",
input: { "file_key": "invoice-template.pdf" },
});
// Delete
ctx.storage.delete({
"storage": "temp-files",
"event": "cleanup",
input: { file_key: ctx.input.tempFileKey },
});
// Upload
var 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
var 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
- TypeScript
- Java
- Go
- .NET
await ctx.messaging.produce({
event: 'order-events:new-order',
message: {
orderId: ctx.input.orderId,
items: ctx.input.items,
},
});
ctx.messaging.produce(Map.of(
"event", "order-events:new-order",
message: Map.of(
orderId: ctx.input.orderId,
items: ctx.input.items
)
));
ctx.messaging.produce({
"event": "order-events:new-order",
message: {
orderId: ctx.input.orderId,
items: ctx.input.items,
},
});
await ctx.messaging.produce({
["event"] = "order-events:new-order",
message: {
orderId: ctx.input.orderId,
items: ctx.input.items,
},
});
Control Flow
Sleep
Pause feature execution:
- TypeScript
- Java
- Go
- .NET
await ctx.sleep(5000); // 5 seconds
await ctx.sleep('5m'); // 5 minutes
await ctx.sleep('1h'); // 1 hour
ctx.sleep(5000); // 5 seconds
ctx.sleep('5m'); // 5 minutes
ctx.sleep('1h'); // 1 hour
ctx.sleep(5000); // 5 seconds
ctx.sleep('5m'); // 5 minutes
ctx.sleep('1h'); // 1 hour
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:
- TypeScript
- Java
- Go
- .NET
const approval = await ctx.waitForSignal('order-approved', {
timeout: '24h',
});
console.log('Approved by:', approval.approvedBy);
Map<String, Object> approval = ctx.waitForSignal('order-approved', Map.of(
"timeout", "24h"
));
System.out.println('Approved by:', approval.approvedBy);
approval := ctx.waitForSignal('order-approved', {
"timeout": "24h",
});
fmt.Println('Approved by:', approval.approvedBy);
var approval = await ctx.waitForSignal('order-approved', {
["timeout"] = "24h",
});
Console.WriteLine('Approved by:', approval.approvedBy);
Send a signal from outside:
- TypeScript
- Java
- Go
- .NET
await ductape.features.signal({
feature_id: 'wf-123',
signal: 'order-approved',
payload: { approvedBy: 'manager@company.com' },
});
ductape.features.signal(Map.of(
"feature_id", "wf-123",
"signal", "order-approved",
payload: Map.of( "approvedBy", "manager@company.com" )
));
client.features.signal({
"feature_id": "wf-123",
"signal": "order-approved",
payload: { "approvedBy": "manager@company.com" },
});
await ductape.features.signal({
["feature_id"] = "wf-123",
["signal"] = "order-approved",
payload: { ["approvedBy"] = "manager@company.com" },
});
Checkpoint
Save state for recovery:
- TypeScript
- Java
- Go
- .NET
await ctx.checkpoint('payment-complete', {
chargeId: payment.id,
timestamp: Date.now(),
});
ctx.checkpoint('payment-complete', Map.of(
chargeId: payment.id,
timestamp: Date.now()
));
ctx.checkpoint('payment-complete', {
chargeId: payment.id,
timestamp: Date.now(),
});
await ctx.checkpoint('payment-complete', {
chargeId: payment.id,
timestamp: Date.now(),
});
Manual Rollback
Trigger rollback programmatically:
- TypeScript
- Java
- Go
- .NET
if (fraudCheck.risk > 0.8) {
await ctx.triggerRollback('High fraud risk detected');
}
if (fraudCheck.risk > 0.8) Map.of(
ctx.triggerRollback('High fraud risk detected');
)
if (fraudCheck.risk > 0.8) {
ctx.triggerRollback('High fraud risk detected');
}
if (fraudCheck.risk > 0.8) {
await ctx.triggerRollback('High fraud risk detected');
}
Executing Features
- TypeScript
- Java
- Go
- .NET
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);
Map<String, Object> result = ductape.features.execute(Map.of(
"tag", "order-fulfillment",
input: Map.of(
"orderId", "ORD-12345",
"amount", 99.99,
"email", "customer@example.com",
items: ['SKU-001', 'SKU-002']
)
));
System.out.println('"Status", ", result.status); // "completed' | 'failed' | 'rolled_back'
System.out.println('"Output", ", result.output);
System.out.println(""Duration", ", result.execution_time, "ms');
System.out.println('Completed Steps:', result.completed_steps);
result := client.features.execute({
"tag": "order-fulfillment",
input: {
"orderId": "ORD-12345",
"amount": 99.99,
"email": "customer@example.com",
items: ['SKU-001', 'SKU-002'],
},
});
fmt.Println('"Status": ", result.status); // "completed' | 'failed' | 'rolled_back'
fmt.Println('"Output": ", result.output);
fmt.Println(""Duration": ", result.execution_time, "ms');
fmt.Println('Completed Steps:', result.completed_steps);
var 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.WriteLine('["Status"] = ", result.status); // "completed' | 'failed' | 'rolled_back'
Console.WriteLine('["Output"] = ", result.output);
Console.WriteLine("["Duration"] = ", result.execution_time, "ms');
Console.WriteLine('Completed Steps:', result.completed_steps);
Conditionals, loops, and switch
You can define if/else (including early return), loops (for), and switch in feature handlers. Because the handler runs once at define time to record steps, use:
- branchOverrides – when you branch on a step result and return early (e.g. "only run payment if validation passed").
- recordInput – when you loop over feature input (e.g.
for (const item of ctx.input.items)). - recordScenarios – when you branch on feature input (e.g. different steps for
type: 'a'vstype: 'b'withif/elseorswitch).
At runtime, steps get conditions (e.g. $Step{validate}{valid} == true, $Input{type} == 'a') and the executor supports ==, !=, >=, <=, >, <, and && / ||. Full syntax and examples: Building Features - Conditionals, loops, and switch.
Next Steps
- Getting Started - Set up your first feature
- Building Features - Definition options, conditionals, loops, switch
- Step Types - Complete reference for all component operations
- Execution & Rollbacks - How execution and rollbacks work
- Examples - Real-world feature patterns
See Also
- Jobs - Schedule features to run automatically