Execution & Rollbacks
This guide explains how features execute, handle failures, and perform automatic rollbacks.
Executing a Feature
Start a feature using ductape.features.execute():
- TypeScript
- Java
- Go
- .NET
const result = await ductape.features.execute({
tag: 'order-fulfillment',
input: {
orderId: 'ORD-12345',
customerId: 'cust-001',
amount: 99.99,
items: ['SKU-001', 'SKU-002'],
},
});
Map<String, Object> result = ductape.features.execute(Map.of(
"tag", "order-fulfillment",
input: Map.of(
"orderId", "ORD-12345",
"customerId", "cust-001",
"amount", 99.99,
items: ['SKU-001', 'SKU-002']
)
));
result := client.features.execute({
"tag": "order-fulfillment",
input: {
"orderId": "ORD-12345",
"customerId": "cust-001",
"amount": 99.99,
items: ['SKU-001', 'SKU-002'],
},
});
var result = await ductape.features.execute({
["tag"] = "order-fulfillment",
input: {
["orderId"] = "ORD-12345",
["customerId"] = "cust-001",
["amount"] = 99.99,
items: ['SKU-001', 'SKU-002'],
},
});
Execution Options
- TypeScript
- Java
- Go
- .NET
const result = await ductape.features.execute({
tag: 'order-fulfillment',
input: { orderId: 'ORD-12345' },
// Optional settings
feature_id: 'custom-id-123', // Custom execution ID
idempotency_key: 'order-12345', // Prevent duplicate executions
timeout: 300000, // Overall timeout (ms)
context: { // Additional context
source: 'api',
requestId: 'req-abc',
},
});
Map<String, Object> result = ductape.features.execute(Map.of(
"tag", "order-fulfillment",
input: Map.of( "orderId", "ORD-12345" ),
// Optional settings
"feature_id", "custom-id-123", // Custom execution ID
"idempotency_key", "order-12345", // Prevent duplicate executions
"timeout", 300000, // Overall timeout (ms)
context: Map.of( // Additional context
"source", "api",
"requestId", "req-abc"
)
));
result := client.features.execute({
"tag": "order-fulfillment",
input: { "orderId": "ORD-12345" },
// Optional settings
"feature_id": "custom-id-123", // Custom execution ID
"idempotency_key": "order-12345", // Prevent duplicate executions
"timeout": 300000, // Overall timeout (ms)
context: { // Additional context
"source": "api",
"requestId": "req-abc",
},
});
var result = await ductape.features.execute({
["tag"] = "order-fulfillment",
input: { ["orderId"] = "ORD-12345" },
// Optional settings
["feature_id"] = "custom-id-123", // Custom execution ID
["idempotency_key"] = "order-12345", // Prevent duplicate executions
["timeout"] = 300000, // Overall timeout (ms)
context: { // Additional context
["source"] = "api",
["requestId"] = "req-abc",
},
});
Execution Result
- TypeScript
- Java
- Go
- .NET
interface FeatureResult {
feature_id: string; // Execution ID
status: 'completed' | 'failed' | 'rolled_back' | 'running';
output?: unknown; // Return value from handler
error?: { // Error details (if failed)
message: string;
step?: string;
code?: string;
};
completed_steps: string[]; // Successfully completed steps
failed_step?: string; // Step that caused failure
execution_time: number; // Duration in milliseconds
rollback_result?: { // Rollback details (if rolled back)
rolled_back_steps: string[];
failed_steps?: Array<{ tag: string; error: string }>;
};
}
interface FeatureResult Map.of(
feature_id: string; // Execution ID
"status", "completed" | 'failed' | 'rolled_back' | 'running';
output?: unknown; // Return value from handler
error?: Map.of( // Error details (if failed)
message: string;
step?: string;
code?: string;
);
completed_steps: string[]; // Successfully completed steps
failed_step?: string; // Step that caused failure
execution_time: number; // Duration in milliseconds
rollback_result?: Map.of( // Rollback details (if rolled back)
rolled_back_steps: string[];
failed_steps?: Array<Map.of( tag: string; error: string )>;
);
)
interface FeatureResult {
feature_id: string; // Execution ID
"status": "completed" | 'failed' | 'rolled_back' | 'running';
output?: unknown; // Return value from handler
error?: { // Error details (if failed)
message: string;
step?: string;
code?: string;
};
completed_steps: string[]; // Successfully completed steps
failed_step?: string; // Step that caused failure
execution_time: number; // Duration in milliseconds
rollback_result?: { // Rollback details (if rolled back)
rolled_back_steps: string[];
failed_steps?: Array<{ tag: string; error: string }>;
};
}
interface FeatureResult {
feature_id: string; // Execution ID
["status"] = "completed" | 'failed' | 'rolled_back' | 'running';
output?: unknown; // Return value from handler
error?: { // Error details (if failed)
message: string;
step?: string;
code?: string;
};
completed_steps: string[]; // Successfully completed steps
failed_step?: string; // Step that caused failure
execution_time: number; // Duration in milliseconds
rollback_result?: { // Rollback details (if rolled back)
rolled_back_steps: string[];
failed_steps?: Array<{ tag: string; error: string }>;
};
}
Example: Handling Results
- TypeScript
- Java
- Go
- .NET
const result = await ductape.features.execute({
tag: 'order-fulfillment',
input: { orderId: 'ORD-12345' },
});
switch (result.status) {
case 'completed':
console.log('Order processed:', result.output);
console.log('Steps completed:', result.completed_steps);
break;
case 'failed':
console.error('Feature failed at step:', result.failed_step);
console.error('Error:', result.error?.message);
break;
case 'rolled_back':
console.log('Feature rolled back');
console.log('Rolled back steps:', result.rollback_result?.rolled_back_steps);
break;
}
Map<String, Object> result = ductape.features.execute(Map.of(
"tag", "order-fulfillment",
input: Map.of( "orderId", "ORD-12345" )
));
switch (result.status) Map.of(
case 'completed':
System.out.println('Order "processed", ", result.output);
System.out.println("Steps "completed", ", result.completed_steps);
break;
case "failed':
console.error('Feature failed at "step", ", result.failed_step);
console.error(""Error", ", result.error?.message);
break;
case "rolled_back':
System.out.println('Feature rolled back');
System.out.println('Rolled back steps:', result.rollback_result?.rolled_back_steps);
break;
)
result := client.features.execute({
"tag": "order-fulfillment",
input: { "orderId": "ORD-12345" },
});
switch (result.status) {
case 'completed':
fmt.Println('Order "processed": ", result.output);
fmt.Println("Steps "completed": ", result.completed_steps);
break;
case "failed':
console.error('Feature failed at "step": ", result.failed_step);
console.error(""Error": ", result.error?.message);
break;
case "rolled_back':
fmt.Println('Feature rolled back');
fmt.Println('Rolled back steps:', result.rollback_result?.rolled_back_steps);
break;
}
var result = await ductape.features.execute({
["tag"] = "order-fulfillment",
input: { ["orderId"] = "ORD-12345" },
});
switch (result.status) {
case 'completed':
Console.WriteLine('Order ["processed"] = ", result.output);
Console.WriteLine("Steps ["completed"] = ", result.completed_steps);
break;
case "failed':
console.error('Feature failed at ["step"] = ", result.failed_step);
console.error("["Error"] = ", result.error?.message);
break;
case "rolled_back':
Console.WriteLine('Feature rolled back');
Console.WriteLine('Rolled back steps:', result.rollback_result?.rolled_back_steps);
break;
}
Feature Lifecycle
┌─────────────────────────────────────────────────────────────────┐
│ FEATURE EXECUTION │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Start │
│ │ │
│ ▼ │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ Step A │ → │ Step B │ → │ Step C │ → Complete │
│ └─────── ────┘ └───────────┘ └───────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ [Rollback A] [Rollback B] [Rollback C] │
│ │
│ If Step C fails: Rollback C → Rollback B → Rollback A │
│ │
└─────────────────────────────────────────────────────────────────┘
Step States
Each step transitions through these states:
| State | Description |
|---|---|
pending | Step not yet started |
running | Step currently executing |
completed | Step finished successfully |
failed | Step failed |
skipped | Step skipped (due to allow_fail) |
rolled_back | Step's rollback handler executed |
Rollback Strategies
Configure how failures trigger rollbacks:
- TypeScript
- Java
- Go
- .NET
await ductape.features.define({
tag: 'my-feature',
options: {
rollback_strategy: 'reverse_all', // Default
},
handler: async (ctx) => { /* ... */ },
});
ductape.features.define(Map.of(
"tag", "my-feature",
options: Map.of(
"rollback_strategy", "reverse_all", // Default
),
handler: async (ctx) => Map.of( /* ... */ )
));
client.features.define({
"tag": "my-feature",
options: {
"rollback_strategy": "reverse_all", // Default
},
handler: async (ctx) => { /* ... */ },
});
await ductape.features.define({
["tag"] = "my-feature",
options: {
["rollback_strategy"] = "reverse_all", // Default
},
handler: async (ctx) => { /* ... */ },
});
Available Strategies
| Strategy | Description |
|---|---|
reverse_all | Roll back all completed steps in reverse order |
critical_only | Only roll back steps marked as critical: true |
to_checkpoint | Roll back to the most recent checkpoint |
none | No automatic rollback |
reverse_all (Default)
Rolls back every completed step that has a rollback handler:
- TypeScript
- Java
- Go
- .NET
await ductape.features.define({
options: { rollback_strategy: 'reverse_all' },
handler: async (ctx) => {
await ctx.step('step-a', handler, rollbackA); // Will rollback
await ctx.step('step-b', handler, rollbackB); // Will rollback
await ctx.step('step-c', handler); // Fails - no rollback
// If step-c fails: rollbackB() → rollbackA()
},
});
ductape.features.define(Map.of(
options: Map.of( "rollback_strategy", "reverse_all" ),
handler: async (ctx) => Map.of(
ctx.step('step-a', handler, rollbackA); // Will rollback
ctx.step('step-b', handler, rollbackB); // Will rollback
ctx.step('step-c', handler); // Fails - no rollback
// If step-c fails: rollbackB() → rollbackA()
)
));
client.features.define({
options: { "rollback_strategy": "reverse_all" },
handler: async (ctx) => {
ctx.step('step-a', handler, rollbackA); // Will rollback
ctx.step('step-b', handler, rollbackB); // Will rollback
ctx.step('step-c', handler); // Fails - no rollback
// If step-c fails: rollbackB() → rollbackA()
},
});
await ductape.features.define({
options: { ["rollback_strategy"] = "reverse_all" },
handler: async (ctx) => {
await ctx.step('step-a', handler, rollbackA); // Will rollback
await ctx.step('step-b', handler, rollbackB); // Will rollback
await ctx.step('step-c', handler); // Fails - no rollback
// If step-c fails: rollbackB() → rollbackA()
},
});
critical_only
Only rolls back steps marked as critical:
- TypeScript
- Java
- Go
- .NET
await ductape.features.define({
options: { rollback_strategy: 'critical_only' },
handler: async (ctx) => {
// Payment is critical - always rollback on failure
await ctx.step('payment', chargeHandler, refundHandler, { critical: true });
// Analytics is not critical - won't rollback
await ctx.step('analytics', trackHandler, rollbackAnalytics, { critical: false });
// Inventory reservation fails
await ctx.step('inventory', reserveHandler);
// Only 'payment' is rolled back
},
});
ductape.features.define(Map.of(
options: Map.of( "rollback_strategy", "critical_only" ),
handler: async (ctx) => Map.of(
// Payment is critical - always rollback on failure
ctx.step('payment', chargeHandler, refundHandler, Map.of( "critical", true ));
// Analytics is not critical - won't rollback
ctx.step('analytics', trackHandler, rollbackAnalytics, Map.of( "critical", false ));
// Inventory reservation fails
ctx.step('inventory', reserveHandler);
// Only 'payment' is rolled back
)
));
client.features.define({
options: { "rollback_strategy": "critical_only" },
handler: async (ctx) => {
// Payment is critical - always rollback on failure
ctx.step('payment', chargeHandler, refundHandler, { "critical": true });
// Analytics is not critical - won't rollback
ctx.step('analytics', trackHandler, rollbackAnalytics, { "critical": false });
// Inventory reservation fails
ctx.step('inventory', reserveHandler);
// Only 'payment' is rolled back
},
});
await ductape.features.define({
options: { ["rollback_strategy"] = "critical_only" },
handler: async (ctx) => {
// Payment is critical - always rollback on failure
await ctx.step('payment', chargeHandler, refundHandler, { ["critical"] = true });
// Analytics is not critical - won't rollback
await ctx.step('analytics', trackHandler, rollbackAnalytics, { ["critical"] = false });
// Inventory reservation fails
await ctx.step('inventory', reserveHandler);
// Only 'payment' is rolled back
},
});
to_checkpoint
Rolls back only to the most recent checkpoint:
- TypeScript
- Java
- Go
- .NET
await ductape.features.define({
options: { rollback_strategy: 'to_checkpoint' },
handler: async (ctx) => {
await ctx.step('create-user', createHandler, deleteUser);
await ctx.checkpoint('user-created'); // Checkpoint here
await ctx.step('payment', chargeHandler, refundHandler);
await ctx.step('inventory', reserveHandler, releaseHandler);
// If inventory fails: only payment is rolled back
// create-user is NOT rolled back (before checkpoint)
},
});
ductape.features.define(Map.of(
options: Map.of( "rollback_strategy", "to_checkpoint" ),
handler: async (ctx) => Map.of(
ctx.step('create-user', createHandler, deleteUser);
ctx.checkpoint('user-created'); // Checkpoint here
ctx.step('payment', chargeHandler, refundHandler);
ctx.step('inventory', reserveHandler, releaseHandler);
// If inventory fails: only payment is rolled back
// create-user is NOT rolled back (before checkpoint)
)
));
client.features.define({
options: { "rollback_strategy": "to_checkpoint" },
handler: async (ctx) => {
ctx.step('create-user', createHandler, deleteUser);
ctx.checkpoint('user-created'); // Checkpoint here
ctx.step('payment', chargeHandler, refundHandler);
ctx.step('inventory', reserveHandler, releaseHandler);
// If inventory fails: only payment is rolled back
// create-user is NOT rolled back (before checkpoint)
},
});
await ductape.features.define({
options: { ["rollback_strategy"] = "to_checkpoint" },
handler: async (ctx) => {
await ctx.step('create-user', createHandler, deleteUser);
await ctx.checkpoint('user-created'); // Checkpoint here
await ctx.step('payment', chargeHandler, refundHandler);
await ctx.step('inventory', reserveHandler, releaseHandler);
// If inventory fails: only payment is rolled back
// create-user is NOT rolled back (before checkpoint)
},
});
none
Disable automatic rollbacks entirely:
- TypeScript
- Java
- Go
- .NET
await ductape.features.define({
options: { rollback_strategy: 'none' },
handler: async (ctx) => {
// No automatic rollbacks - handle failures manually
try {
await ctx.step('risky-operation', handler);
} catch (error) {
// Manual cleanup
await ctx.triggerRollback('Manual rollback triggered');
}
},
});
ductape.features.define(Map.of(
options: Map.of( "rollback_strategy", "none" ),
handler: async (ctx) => Map.of(
// No automatic rollbacks - handle failures manually
try Map.of(
ctx.step('risky-operation', handler);
) catch (error) Map.of(
// Manual cleanup
ctx.triggerRollback('Manual rollback triggered');
)
)
));
client.features.define({
options: { "rollback_strategy": "none" },
handler: async (ctx) => {
// No automatic rollbacks - handle failures manually
try {
ctx.step('risky-operation', handler);
} catch (error) {
// Manual cleanup
ctx.triggerRollback('Manual rollback triggered');
}
},
});
await ductape.features.define({
options: { ["rollback_strategy"] = "none" },
handler: async (ctx) => {
// No automatic rollbacks - handle failures manually
try {
await ctx.step('risky-operation', handler);
} catch (error) {
// Manual cleanup
await ctx.triggerRollback('Manual rollback triggered');
}
},
});
Step Options for Failure Handling
allow_fail
Continue feature execution even if the step fails:
- TypeScript
- Java
- Go
- .NET
await ctx.step(
'send-notification',
async () => {
await ctx.notification.email({ /* ... */ });
},
null,
{ allow_fail: true } // Feature continues if this fails
);
ctx.step(
'send-notification',
async () => Map.of(
ctx.notification.email(Map.of( /* ... */ ));
),
null,
Map.of( "allow_fail", true ) // Feature continues if this fails
);
ctx.step(
'send-notification',
async () => {
ctx.notification.email({ /* ... */ });
},
null,
{ "allow_fail": true } // Feature continues if this fails
);
await ctx.step(
'send-notification',
async () => {
await ctx.notification.email({ /* ... */ });
},
null,
{ ["allow_fail"] = true } // Feature continues if this fails
);
retries and retry_delay
Automatically retry failed steps:
- TypeScript
- Java
- Go
- .NET
await ctx.step(
'external-api-call',
async () => {
return ctx.api.run({
app: 'external-service',
event: 'fetch-data',
input: {},
});
},
null,
{
retries: 3, // Retry up to 3 times
retry_delay: 2000, // Wait 2 seconds between retries
}
);
ctx.step(
'external-api-call',
async () => Map.of(
return ctx.api.run(Map.of(
"app", "external-service",
"event", "fetch-data",
input: Map.of()
));
),
null,
Map.of(
"retries", 3, // Retry up to 3 times
"retry_delay", 2000, // Wait 2 seconds between retries
)
);
ctx.step(
'external-api-call',
async () => {
return ctx.api.run({
"app": "external-service",
"event": "fetch-data",
input: {},
});
},
null,
{
"retries": 3, // Retry up to 3 times
"retry_delay": 2000, // Wait 2 seconds between retries
}
);
await ctx.step(
'external-api-call',
async () => {
return ctx.api.run({
["app"] = "external-service",
["event"] = "fetch-data",
input: {},
});
},
null,
{
["retries"] = 3, // Retry up to 3 times
["retry_delay"] = 2000, // Wait 2 seconds between retries
}
);
timeout
Set a maximum duration for step execution:
- TypeScript
- Java
- Go
- .NET
await ctx.step(
'slow-operation',
async () => {
return ctx.api.run({
app: 'slow-service',
event: 'process',
input: {},
});
},
null,
{ timeout: 30000 } // 30 second timeout
);
ctx.step(
'slow-operation',
async () => Map.of(
return ctx.api.run(Map.of(
"app", "slow-service",
"event", "process",
input: Map.of()
));
),
null,
Map.of( "timeout", 30000 ) // 30 second timeout
);
ctx.step(
'slow-operation',
async () => {
return ctx.api.run({
"app": "slow-service",
"event": "process",
input: {},
});
},
null,
{ "timeout": 30000 } // 30 second timeout
);
await ctx.step(
'slow-operation',
async () => {
return ctx.api.run({
["app"] = "slow-service",
["event"] = "process",
input: {},
});
},
null,
{ ["timeout"] = 30000 } // 30 second timeout
);
critical
Mark a step as always requiring rollback:
- TypeScript
- Java
- Go
- .NET
// With 'critical_only' strategy, only critical steps roll back
await ctx.step(
'payment',
async () => ctx.api.run({ app: 'stripe', event: 'charge', input: {} }),
async (result) => ctx.api.run({ app: 'stripe', event: 'refund', input: { body: { id: result.id } } }),
{ critical: true }
);
// With 'critical_only' strategy, only critical steps roll back
ctx.step(
'payment',
async () => ctx.api.run(Map.of( "app", "stripe", "event", "charge", input: Map.of() )),
async (result) => ctx.api.run(Map.of( "app", "stripe", "event", "refund", input: Map.of( body: Map.of( id: result.id ) ) )),
Map.of( "critical", true )
);
// With 'critical_only' strategy, only critical steps roll back
ctx.step(
'payment',
async () => ctx.api.run({ "app": "stripe", "event": "charge", input: {} }),
async (result) => ctx.api.run({ "app": "stripe", "event": "refund", input: { body: { id: result.id } } }),
{ "critical": true }
);
// With 'critical_only' strategy, only critical steps roll back
await ctx.step(
'payment',
async () => ctx.api.run({ ["app"] = "stripe", ["event"] = "charge", input: {} }),
async (result) => ctx.api.run({ ["app"] = "stripe", ["event"] = "refund", input: { body: { id: result.id } } }),
{ ["critical"] = true }
);
Manual Rollback
Trigger rollback programmatically using ctx.triggerRollback():
- TypeScript
- Java
- Go
- .NET
handler: async (ctx) => {
const payment = await ctx.step('payment', chargeHandler, refundHandler);
// Check for fraud
const fraudCheck = await ctx.step('fraud-check', async () => {
return ctx.api.run({
app: 'fraud-detection',
event: 'analyze',
input: { body: { transactionId: payment.id } },
});
});
if (fraudCheck.riskScore > 0.8) {
// Manually trigger rollback
const result = await ctx.triggerRollback('High fraud risk detected');
return {
success: false,
reason: 'fraud_detected',
rolledBackSteps: result.rolled_back_steps,
};
}
// Continue with order...
}
handler: async (ctx) => Map.of(
Map<String, Object> payment = ctx.step('payment', chargeHandler, refundHandler);
// Check for fraud
Map<String, Object> fraudCheck = ctx.step('fraud-check', async () => Map.of(
return ctx.api.run(Map.of(
"app", "fraud-detection",
"event", "analyze",
input: Map.of( body: Map.of( transactionId: payment.id ) )
));
));
if (fraudCheck.riskScore > 0.8) Map.of(
// Manually trigger rollback
Map<String, Object> result = ctx.triggerRollback('High fraud risk detected');
return Map.of(
"success", false,
"reason", "fraud_detected",
rolledBackSteps: result.rolled_back_steps
);
)
// Continue with order...
)
handler: async (ctx) => {
payment := ctx.step('payment', chargeHandler, refundHandler);
// Check for fraud
fraudCheck := ctx.step('fraud-check', async () => {
return ctx.api.run({
"app": "fraud-detection",
"event": "analyze",
input: { body: { transactionId: payment.id } },
});
});
if (fraudCheck.riskScore > 0.8) {
// Manually trigger rollback
result := ctx.triggerRollback('High fraud risk detected');
return {
"success": false,
"reason": "fraud_detected",
rolledBackSteps: result.rolled_back_steps,
};
}
// Continue with order...
}
handler: async (ctx) => {
var payment = await ctx.step('payment', chargeHandler, refundHandler);
// Check for fraud
var fraudCheck = await ctx.step('fraud-check', async () => {
return ctx.api.run({
["app"] = "fraud-detection",
["event"] = "analyze",
input: { body: { transactionId: payment.id } },
});
});
if (fraudCheck.riskScore > 0.8) {
// Manually trigger rollback
var result = await ctx.triggerRollback('High fraud risk detected');
return {
["success"] = false,
["reason"] = "fraud_detected",
rolledBackSteps: result.rolled_back_steps,
};
}
// Continue with order...
}
Rollback Result
- TypeScript
- Java
- Go
- .NET
interface RollbackResult {
success: boolean;
rolled_back_steps: string[];
failed_steps?: Array<{
tag: string;
error: string;
}>;
reason: string;
}
interface RollbackResult Map.of(
success: boolean;
rolled_back_steps: string[];
failed_steps?: Array<Map.of(
tag: string;
error: string;
)>;
reason: string;
)
interface RollbackResult {
success: boolean;
rolled_back_steps: string[];
failed_steps?: Array<{
tag: string;
error: string;
}>;
reason: string;
}
interface RollbackResult {
success: boolean;
rolled_back_steps: string[];
failed_steps?: Array<{
tag: string;
error: string;
}>;
reason: string;
}
Checkpoints
Create checkpoints to save progress and control rollback scope:
- TypeScript
- Java
- Go
- .NET
handler: async (ctx) => {
// Phase 1: User setup
await ctx.step('create-user', createUser, deleteUser);
await ctx.step('verify-email', sendVerification);
// Checkpoint after user is fully created
await ctx.checkpoint('user-setup-complete', {
userId: ctx.steps['create-user'].id,
});
// Phase 2: Payment processing
await ctx.step('payment', chargeCard, refundCard);
// Checkpoint after payment
await ctx.checkpoint('payment-complete', {
chargeId: ctx.steps['payment'].id,
});
// Phase 3: Fulfillment
await ctx.step('ship-order', shipOrder, cancelShipment);
// If shipping fails with 'to_checkpoint' strategy:
// - Only 'ship-order' is rolled back
// - User and payment remain intact
}
handler: async (ctx) => Map.of(
// Phase 1: User setup
ctx.step('create-user', createUser, deleteUser);
ctx.step('verify-email', sendVerification);
// Checkpoint after user is fully created
ctx.checkpoint('user-setup-complete', Map.of(
userId: ctx.steps['create-user'].id
));
// Phase 2: Payment processing
ctx.step('payment', chargeCard, refundCard);
// Checkpoint after payment
ctx.checkpoint('payment-complete', Map.of(
chargeId: ctx.steps['payment'].id
));
// Phase 3: Fulfillment
ctx.step('ship-order', shipOrder, cancelShipment);
// If shipping fails with 'to_checkpoint' strategy:
// - Only 'ship-order' is rolled back
// - User and payment remain intact
)
handler: async (ctx) => {
// Phase 1: User setup
ctx.step('create-user', createUser, deleteUser);
ctx.step('verify-email', sendVerification);
// Checkpoint after user is fully created
ctx.checkpoint('user-setup-complete', {
userId: ctx.steps['create-user'].id,
});
// Phase 2: Payment processing
ctx.step('payment', chargeCard, refundCard);
// Checkpoint after payment
ctx.checkpoint('payment-complete', {
chargeId: ctx.steps['payment'].id,
});
// Phase 3: Fulfillment
ctx.step('ship-order', shipOrder, cancelShipment);
// If shipping fails with 'to_checkpoint' strategy:
// - Only 'ship-order' is rolled back
// - User and payment remain intact
}
handler: async (ctx) => {
// Phase 1: User setup
await ctx.step('create-user', createUser, deleteUser);
await ctx.step('verify-email', sendVerification);
// Checkpoint after user is fully created
await ctx.checkpoint('user-setup-complete', {
userId: ctx.steps['create-user'].id,
});
// Phase 2: Payment processing
await ctx.step('payment', chargeCard, refundCard);
// Checkpoint after payment
await ctx.checkpoint('payment-complete', {
chargeId: ctx.steps['payment'].id,
});
// Phase 3: Fulfillment
await ctx.step('ship-order', shipOrder, cancelShipment);
// If shipping fails with 'to_checkpoint' strategy:
// - Only 'ship-order' is rolled back
// - User and payment remain intact
}
Accessing Checkpoints
- TypeScript
- Java
- Go
- .NET
// In your handler
const lastCheckpoint = ctx.checkpoint_name; // e.g., 'payment-complete'
const checkpointData = ctx.checkpoint_data; // { chargeId: '...' }
// In your handler
Map<String, Object> lastCheckpoint = ctx.checkpoint_name; // e.g., 'payment-complete'
Map<String, Object> checkpointData = ctx.checkpoint_data; // Map.of( "chargeId", "..." )
// In your handler
lastCheckpoint := ctx.checkpoint_name; // e.g., 'payment-complete'
checkpointData := ctx.checkpoint_data; // { "chargeId": "..." }
// In your handler
var lastCheckpoint = ctx.checkpoint_name; // e.g., 'payment-complete'
var checkpointData = ctx.checkpoint_data; // { ["chargeId"] = "..." }
Signals
Wait for external events during feature execution:
Waiting for Signals
- TypeScript
- Java
- Go
- .NET
handler: async (ctx) => {
// Create order
const order = await ctx.step('create-order', createOrder);
// Wait for manual approval
const approval = await ctx.waitForSignal('order-approved', {
timeout: '24h',
});
console.log('Approved by:', approval.approvedBy);
console.log('Comments:', approval.comments);
// Continue with fulfillment
await ctx.step('fulfill', fulfillOrder);
}
handler: async (ctx) => Map.of(
// Create order
Map<String, Object> order = ctx.step('create-order', createOrder);
// Wait for manual approval
Map<String, Object> approval = ctx.waitForSignal('order-approved', Map.of(
"timeout", "24h"
));
System.out.println('Approved "by", ", approval.approvedBy);
System.out.println(""Comments", ", approval.comments);
// Continue with fulfillment
ctx.step("fulfill', fulfillOrder);
)
handler: async (ctx) => {
// Create order
order := ctx.step('create-order', createOrder);
// Wait for manual approval
approval := ctx.waitForSignal('order-approved', {
"timeout": "24h",
});
fmt.Println('Approved "by": ", approval.approvedBy);
fmt.Println(""Comments": ", approval.comments);
// Continue with fulfillment
ctx.step("fulfill', fulfillOrder);
}
handler: async (ctx) => {
// Create order
var order = await ctx.step('create-order', createOrder);
// Wait for manual approval
var approval = await ctx.waitForSignal('order-approved', {
["timeout"] = "24h",
});
Console.WriteLine('Approved ["by"] = ", approval.approvedBy);
Console.WriteLine("["Comments"] = ", approval.comments);
// Continue with fulfillment
await ctx.step("fulfill', fulfillOrder);
}
Sending Signals
From outside the feature, send a signal to continue execution:
- TypeScript
- Java
- Go
- .NET
await ductape.features.signal({
feature_id: 'wf-abc123',
signal: 'order-approved',
payload: {
approvedBy: 'manager@company.com',
comments: 'Approved for processing',
timestamp: Date.now(),
},
});
ductape.features.signal(Map.of(
"feature_id", "wf-abc123",
"signal", "order-approved",
payload: Map.of(
"approvedBy", "manager@company.com",
"comments", "Approved for processing",
timestamp: Date.now()
)
));
client.features.signal({
"feature_id": "wf-abc123",
"signal": "order-approved",
payload: {
"approvedBy": "manager@company.com",
"comments": "Approved for processing",
timestamp: Date.now(),
},
});
await ductape.features.signal({
["feature_id"] = "wf-abc123",
["signal"] = "order-approved",
payload: {
["approvedBy"] = "manager@company.com",
["comments"] = "Approved for processing",
timestamp: Date.now(),
},
});
Multiple Signals
Wait for any of multiple possible signals:
- TypeScript
- Java
- Go
- .NET
const result = await ctx.waitForSignal(['approved', 'rejected', 'escalated'], {
timeout: '48h',
});
// Check which signal was received
if (result._signal === 'approved') {
await ctx.step('process', processOrder);
} else if (result._signal === 'rejected') {
await ctx.triggerRollback('Order rejected');
} else if (result._signal === 'escalated') {
await ctx.step('notify-manager', notifyManager);
}
Map<String, Object> result = ctx.waitForSignal(['approved', 'rejected', 'escalated'], Map.of(
"timeout", "48h"
));
// Check which signal was received
if (result._signal === 'approved') Map.of(
ctx.step('process', processOrder);
) else if (result._signal === 'rejected') Map.of(
ctx.triggerRollback('Order rejected');
) else if (result._signal === 'escalated') Map.of(
ctx.step('notify-manager', notifyManager);
)
result := ctx.waitForSignal(['approved', 'rejected', 'escalated'], {
"timeout": "48h",
});
// Check which signal was received
if (result._signal === 'approved') {
ctx.step('process', processOrder);
} else if (result._signal === 'rejected') {
ctx.triggerRollback('Order rejected');
} else if (result._signal === 'escalated') {
ctx.step('notify-manager', notifyManager);
}
var result = await ctx.waitForSignal(['approved', 'rejected', 'escalated'], {
["timeout"] = "48h",
});
// Check which signal was received
if (result._signal === 'approved') {
await ctx.step('process', processOrder);
} else if (result._signal === 'rejected') {
await ctx.triggerRollback('Order rejected');
} else if (result._signal === 'escalated') {
await ctx.step('notify-manager', notifyManager);
}
Querying Feature Status
Check the status of running or completed features:
- TypeScript
- Java
- Go
- .NET
// Get feature status
const status = await ductape.features.status({
feature_id: 'wf-abc123',
});
console.log('Status:', status.status);
console.log('Current step:', status.current_step);
console.log('Completed steps:', status.completed_steps);
console.log('State:', status.state);
// Get feature status
Map<String, Object> status = ductape.features.status(Map.of(
"feature_id", "wf-abc123"
));
System.out.println('"Status", ", status.status);
System.out.println("Current "step", ", status.current_step);
System.out.println("Completed "steps", ", status.completed_steps);
System.out.println("State:', status.state);
// Get feature status
status := client.features.status({
"feature_id": "wf-abc123",
});
fmt.Println('"Status": ", status.status);
fmt.Println("Current "step": ", status.current_step);
fmt.Println("Completed "steps": ", status.completed_steps);
fmt.Println("State:', status.state);
// Get feature status
var status = await ductape.features.status({
["feature_id"] = "wf-abc123",
});
Console.WriteLine('["Status"] = ", status.status);
Console.WriteLine("Current ["step"] = ", status.current_step);
Console.WriteLine("Completed ["steps"] = ", status.completed_steps);
Console.WriteLine("State:', status.state);
Query Handlers
Define queries in your feature definition:
- TypeScript
- Java
- Go
- .NET
await ductape.features.define({
tag: 'order-feature',
queries: {
'getProgress': {
handler: (ctx) => ({
completedSteps: ctx.completed_steps.length,
totalSteps: 5,
currentStep: ctx.current_step,
percentComplete: (ctx.completed_steps.length / 5) * 100,
}),
},
'getOrderDetails': {
handler: (ctx) => ({
orderId: ctx.input.orderId,
status: ctx.state.orderStatus,
paymentId: ctx.steps['payment']?.id,
}),
},
},
handler: async (ctx) => { /* ... */ },
});
// Query from outside
const progress = await ductape.features.query({
feature_id: 'wf-abc123',
query: 'getProgress',
});
ductape.features.define(Map.of(
"tag", "order-feature",
queries: Map.of(
'getProgress': Map.of(
handler: (ctx) => (Map.of(
completedSteps: ctx.completed_steps.length,
"totalSteps", 5,
currentStep: ctx.current_step,
percentComplete: (ctx.completed_steps.length / 5) * 100
))
),
'getOrderDetails': Map.of(
handler: (ctx) => (Map.of(
orderId: ctx.input.orderId,
status: ctx.state.orderStatus,
paymentId: ctx.steps['payment']?.id
))
)
),
handler: async (ctx) => Map.of( /* ... */ )
));
// Query from outside
Map<String, Object> progress = ductape.features.query(Map.of(
"feature_id", "wf-abc123",
"query", "getProgress"
));
client.features.define({
"tag": "order-feature",
queries: {
'getProgress': {
handler: (ctx) => ({
completedSteps: ctx.completed_steps.length,
"totalSteps": 5,
currentStep: ctx.current_step,
percentComplete: (ctx.completed_steps.length / 5) * 100,
}),
},
'getOrderDetails': {
handler: (ctx) => ({
orderId: ctx.input.orderId,
status: ctx.state.orderStatus,
paymentId: ctx.steps['payment']?.id,
}),
},
},
handler: async (ctx) => { /* ... */ },
});
// Query from outside
progress := client.features.query({
"feature_id": "wf-abc123",
"query": "getProgress",
});
await ductape.features.define({
["tag"] = "order-feature",
queries: {
'getProgress': {
handler: (ctx) => ({
completedSteps: ctx.completed_steps.length,
["totalSteps"] = 5,
currentStep: ctx.current_step,
percentComplete: (ctx.completed_steps.length / 5) * 100,
}),
},
'getOrderDetails': {
handler: (ctx) => ({
orderId: ctx.input.orderId,
status: ctx.state.orderStatus,
paymentId: ctx.steps['payment']?.id,
}),
},
},
handler: async (ctx) => { /* ... */ },
});
// Query from outside
var progress = await ductape.features.query({
["feature_id"] = "wf-abc123",
["query"] = "getProgress",
});
Cancelling Features
Cancel a running feature:
- TypeScript
- Java
- Go
- .NET
const result = await ductape.features.cancel({
feature_id: 'wf-abc123',
reason: 'User requested cancellation',
});
console.log('Cancelled:', result.cancelled);
console.log('Rolled back steps:', result.rolled_back_steps);
Map<String, Object> result = ductape.features.cancel(Map.of(
"feature_id", "wf-abc123",
"reason", "User requested cancellation"
));
System.out.println('"Cancelled", ", result.cancelled);
System.out.println("Rolled back steps:', result.rolled_back_steps);
result := client.features.cancel({
"feature_id": "wf-abc123",
"reason": "User requested cancellation",
});
fmt.Println('"Cancelled": ", result.cancelled);
fmt.Println("Rolled back steps:', result.rolled_back_steps);
var result = await ductape.features.cancel({
["feature_id"] = "wf-abc123",
["reason"] = "User requested cancellation",
});
Console.WriteLine('["Cancelled"] = ", result.cancelled);
Console.WriteLine("Rolled back steps:', result.rolled_back_steps);
Dispatching Features
Dispatch features to run on a schedule or at a specific time:
Schedule for Later
- TypeScript
- Java
- Go
- .NET
// Run feature in 1 hour
const result = await ductape.features.dispatch({
feature: 'order-fulfillment',
input: { orderId: 'ORD-123' },
schedule: {
start_at: Date.now() + 3600000, // 1 hour from now
},
});
console.log('Scheduled job ID:', result.job_id);
console.log('Scheduled for:', result.scheduled_at);
// Run feature in 1 hour
Map<String, Object> result = ductape.features.dispatch(Map.of(
"feature", "order-fulfillment",
input: Map.of( "orderId", "ORD-123" ),
schedule: Map.of(
start_at: Date.now() + 3600000, // 1 hour from now
)
));
System.out.println('Scheduled job "ID", ", result.job_id);
System.out.println("Scheduled for:', result.scheduled_at);
// Run feature in 1 hour
result := client.features.dispatch({
"feature": "order-fulfillment",
input: { "orderId": "ORD-123" },
schedule: {
start_at: Date.now() + 3600000, // 1 hour from now
},
});
fmt.Println('Scheduled job "ID": ", result.job_id);
fmt.Println("Scheduled for:', result.scheduled_at);
// Run feature in 1 hour
var result = await ductape.features.dispatch({
["feature"] = "order-fulfillment",
input: { ["orderId"] = "ORD-123" },
schedule: {
start_at: Date.now() + 3600000, // 1 hour from now
},
});
Console.WriteLine('Scheduled job ["ID"] = ", result.job_id);
Console.WriteLine("Scheduled for:', result.scheduled_at);
Cron Schedule
- TypeScript
- Java
- Go
- .NET
// Run daily at midnight
const result = await ductape.features.dispatch({
feature: 'daily-report',
input: { reportType: 'sales' },
schedule: {
cron: '0 0 * * *', // Every day at midnight
},
});
// Run daily at midnight
Map<String, Object> result = ductape.features.dispatch(Map.of(
"feature", "daily-report",
input: Map.of( "reportType", "sales" ),
schedule: Map.of(
"cron", "0 0 * * *", // Every day at midnight
)
));
// Run daily at midnight
result := client.features.dispatch({
"feature": "daily-report",
input: { "reportType": "sales" },
schedule: {
"cron": "0 0 * * *", // Every day at midnight
},
});
// Run daily at midnight
var result = await ductape.features.dispatch({
["feature"] = "daily-report",
input: { ["reportType"] = "sales" },
schedule: {
["cron"] = "0 0 * * *", // Every day at midnight
},
});
Recurring Dispatch
- TypeScript
- Java
- Go
- .NET
// Run every hour with retry options
const result = await ductape.features.dispatch({
feature: 'sync-inventory',
input: { source: 'warehouse-a' },
schedule: {
cron: '0 * * * *', // Every hour
timezone: 'America/New_York',
},
options: {
retries: 3,
timeout: 300000,
},
});
// Run every hour with retry options
Map<String, Object> result = ductape.features.dispatch(Map.of(
"feature", "sync-inventory",
input: Map.of( "source", "warehouse-a" ),
schedule: Map.of(
"cron", "0 * * * *", // Every hour
"timezone", "America/New_York"
),
options: Map.of(
"retries", 3,
"timeout", 300000
)
));
// Run every hour with retry options
result := client.features.dispatch({
"feature": "sync-inventory",
input: { "source": "warehouse-a" },
schedule: {
"cron": "0 * * * *", // Every hour
"timezone": "America/New_York",
},
options: {
"retries": 3,
"timeout": 300000,
},
});
// Run every hour with retry options
var result = await ductape.features.dispatch({
["feature"] = "sync-inventory",
input: { ["source"] = "warehouse-a" },
schedule: {
["cron"] = "0 * * * *", // Every hour
["timezone"] = "America/New_York",
},
options: {
["retries"] = 3,
["timeout"] = 300000,
},
});
Replay, Restart & Resume
Ductape Features track execution state persistently, enabling powerful recovery capabilities:
| Operation | Description | Input | State |
|---|---|---|---|
| Replay | Re-execute with same input | Original | Fresh |
| Restart | Re-execute with new/modified input | New/Modified | Fresh |
| Resume | Continue from last state | Original | Preserved |
Replay Feature
Re-execute a feature using the same input from a previous execution:
- TypeScript
- Java
- Go
- .NET
// Replay a failed feature
const replayResult = await ductape.features.replay({
feature_id: 'wf-abc123',
reason: 'Debugging payment failure',
});
console.log(replayResult.feature_id); // New feature ID
console.log(replayResult.replayed_from); // Original: 'wf-abc123'
// Replay a failed feature
Map<String, Object> replayResult = ductape.features.replay(Map.of(
"feature_id", "wf-abc123",
"reason", "Debugging payment failure"
));
System.out.println(replayResult.feature_id); // New feature ID
System.out.println(replayResult.replayed_from); // "Original", "wf-abc123"
// Replay a failed feature
replayResult := client.features.replay({
"feature_id": "wf-abc123",
"reason": "Debugging payment failure",
});
fmt.Println(replayResult.feature_id); // New feature ID
fmt.Println(replayResult.replayed_from); // "Original": "wf-abc123"
// Replay a failed feature
var replayResult = await ductape.features.replay({
["feature_id"] = "wf-abc123",
["reason"] = "Debugging payment failure",
});
Console.WriteLine(replayResult.feature_id); // New feature ID
Console.WriteLine(replayResult.replayed_from); // ["Original"] = "wf-abc123"
Replay with Options
- TypeScript
- Java
- Go
- .NET
const replayResult = await ductape.features.replay({
feature_id: 'wf-abc123',
options: {
retries: 5,
timeout: 120000,
debug: true,
},
reason: 'Debugging payment failure',
idempotency_key: 'replay-wf-abc123-attempt-2',
});
Map<String, Object> replayResult = ductape.features.replay(Map.of(
"feature_id", "wf-abc123",
options: Map.of(
"retries", 5,
"timeout", 120000,
"debug", true
),
"reason", "Debugging payment failure",
"idempotency_key", "replay-wf-abc123-attempt-2"
));
replayResult := client.features.replay({
"feature_id": "wf-abc123",
options: {
"retries": 5,
"timeout": 120000,
"debug": true,
},
"reason": "Debugging payment failure",
"idempotency_key": "replay-wf-abc123-attempt-2",
});
var replayResult = await ductape.features.replay({
["feature_id"] = "wf-abc123",
options: {
["retries"] = 5,
["timeout"] = 120000,
["debug"] = true,
},
["reason"] = "Debugging payment failure",
["idempotency_key"] = "replay-wf-abc123-attempt-2",
});
Detecting Replay in Handler
- TypeScript
- Java
- Go
- .NET
handler: async (ctx) => {
if (ctx.is_replay) {
ctx.log.info('Replaying feature', {
original_id: ctx.replayed_from,
reason: ctx.replay_reason,
});
}
// Normal feature logic...
}
handler: async (ctx) => Map.of(
if (ctx.is_replay) Map.of(
ctx.log.info('Replaying feature', Map.of(
original_id: ctx.replayed_from,
reason: ctx.replay_reason
));
)
// Normal feature logic...
)
handler: async (ctx) => {
if (ctx.is_replay) {
ctx.log.info('Replaying feature', {
original_id: ctx.replayed_from,
reason: ctx.replay_reason,
});
}
// Normal feature logic...
}
handler: async (ctx) => {
if (ctx.is_replay) {
ctx.log.info('Replaying feature', {
original_id: ctx.replayed_from,
reason: ctx.replay_reason,
});
}
// Normal feature logic...
}
Restart Feature
Reset and re-execute with new or modified input:
- TypeScript
- Java
- Go
- .NET
// Restart with corrected input
const restartResult = await ductape.features.restart({
feature_id: 'wf-abc123',
input: {
orderId: 'ORD-123',
email: 'corrected@email.com', // Fixed email
},
reason: 'Customer email was incorrect',
});
// Restart with corrected input
Map<String, Object> restartResult = ductape.features.restart(Map.of(
"feature_id", "wf-abc123",
input: Map.of(
"orderId", "ORD-123",
"email", "corrected@email.com", // Fixed email
),
"reason", "Customer email was incorrect"
));
// Restart with corrected input
restartResult := client.features.restart({
"feature_id": "wf-abc123",
input: {
"orderId": "ORD-123",
"email": "corrected@email.com", // Fixed email
},
"reason": "Customer email was incorrect",
});
// Restart with corrected input
var restartResult = await ductape.features.restart({
["feature_id"] = "wf-abc123",
input: {
["orderId"] = "ORD-123",
["email"] = "corrected@email.com", // Fixed email
},
["reason"] = "Customer email was incorrect",
});
Partial Input Override
- TypeScript
- Java
- Go
- .NET
// Only override specific fields, merge with original
const restartResult = await ductape.features.restart({
feature_id: 'wf-abc123',
input_override: {
email: 'corrected@email.com',
},
merge_input: true,
});
// Only override specific fields, merge with original
Map<String, Object> restartResult = ductape.features.restart(Map.of(
"feature_id", "wf-abc123",
input_override: Map.of(
"email", "corrected@email.com"
),
"merge_input", true
));
// Only override specific fields, merge with original
restartResult := client.features.restart({
"feature_id": "wf-abc123",
input_override: {
"email": "corrected@email.com",
},
"merge_input": true,
});
// Only override specific fields, merge with original
var restartResult = await ductape.features.restart({
["feature_id"] = "wf-abc123",
input_override: {
["email"] = "corrected@email.com",
},
["merge_input"] = true,
});
Resume Feature
Continue a feature from where it stopped (paused, failed, or crashed):
- TypeScript
- Java
- Go
- .NET
// Resume a paused feature
const resumeResult = await ductape.features.resume({
feature_id: 'wf-abc123',
});
// Resume a paused feature
Map<String, Object> resumeResult = ductape.features.resume(Map.of(
"feature_id", "wf-abc123"
));
// Resume a paused feature
resumeResult := client.features.resume({
"feature_id": "wf-abc123",
});
// Resume a paused feature
var resumeResult = await ductape.features.resume({
["feature_id"] = "wf-abc123",
});
Resume from Checkpoint
- TypeScript
- Java
- Go
- .NET
// Resume from a specific checkpoint
const resumeResult = await ductape.features.resume({
feature_id: 'wf-abc123',
from_checkpoint: 'payment-complete',
input: {
retry_payment: false,
},
});
// Resume from a specific checkpoint
Map<String, Object> resumeResult = ductape.features.resume(Map.of(
"feature_id", "wf-abc123",
"from_checkpoint", "payment-complete",
input: Map.of(
"retry_payment", false
)
));
// Resume from a specific checkpoint
resumeResult := client.features.resume({
"feature_id": "wf-abc123",
"from_checkpoint": "payment-complete",
input: {
"retry_payment": false,
},
});
// Resume from a specific checkpoint
var resumeResult = await ductape.features.resume({
["feature_id"] = "wf-abc123",
["from_checkpoint"] = "payment-complete",
input: {
["retry_payment"] = false,
},
});
Resume from Failed Step
- TypeScript
- Java
- Go
- .NET
// Resume from the step that failed
const resumeResult = await ductape.features.resume({
feature_id: 'wf-abc123',
from_step: 'create-shipment',
skip_steps: ['send-confirmation'], // Skip problematic step
});
// Resume from the step that failed
Map<String, Object> resumeResult = ductape.features.resume(Map.of(
"feature_id", "wf-abc123",
"from_step", "create-shipment",
skip_steps: ['send-confirmation'], // Skip problematic step
));
// Resume from the step that failed
resumeResult := client.features.resume({
"feature_id": "wf-abc123",
"from_step": "create-shipment",
skip_steps: ['send-confirmation'], // Skip problematic step
});
// Resume from the step that failed
var resumeResult = await ductape.features.resume({
["feature_id"] = "wf-abc123",
["from_step"] = "create-shipment",
skip_steps: ['send-confirmation'], // Skip problematic step
});
Handling Resume in Handler
- TypeScript
- Java
- Go
- .NET
handler: async (ctx) => {
if (ctx.is_restored) {
ctx.log.info('Resuming feature', {
from_checkpoint: ctx.restored_checkpoint?.name,
completed_steps: ctx.completed_steps,
});
}
// Check if step already completed before running
if (!ctx.completed_steps.includes('validate-order')) {
await ctx.step('validate-order', async () => {
// Validation logic...
});
}
await ctx.checkpoint('validation-complete');
if (!ctx.completed_steps.includes('process-payment')) {
await ctx.step('process-payment', async () => {
// Payment logic...
});
}
// Continue with remaining steps...
}
handler: async (ctx) => Map.of(
if (ctx.is_restored) Map.of(
ctx.log.info('Resuming feature', Map.of(
from_checkpoint: ctx.restored_checkpoint?.name,
completed_steps: ctx.completed_steps
));
)
// Check if step already completed before running
if (!ctx.completed_steps.includes('validate-order')) Map.of(
ctx.step('validate-order', async () => Map.of(
// Validation logic...
));
)
ctx.checkpoint('validation-complete');
if (!ctx.completed_steps.includes('process-payment')) Map.of(
ctx.step('process-payment', async () => Map.of(
// Payment logic...
));
)
// Continue with remaining steps...
)
handler: async (ctx) => {
if (ctx.is_restored) {
ctx.log.info('Resuming feature', {
from_checkpoint: ctx.restored_checkpoint?.name,
completed_steps: ctx.completed_steps,
});
}
// Check if step already completed before running
if (!ctx.completed_steps.includes('validate-order')) {
ctx.step('validate-order', async () => {
// Validation logic...
});
}
ctx.checkpoint('validation-complete');
if (!ctx.completed_steps.includes('process-payment')) {
ctx.step('process-payment', async () => {
// Payment logic...
});
}
// Continue with remaining steps...
}
handler: async (ctx) => {
if (ctx.is_restored) {
ctx.log.info('Resuming feature', {
from_checkpoint: ctx.restored_checkpoint?.name,
completed_steps: ctx.completed_steps,
});
}
// Check if step already completed before running
if (!ctx.completed_steps.includes('validate-order')) {
await ctx.step('validate-order', async () => {
// Validation logic...
});
}
await ctx.checkpoint('validation-complete');
if (!ctx.completed_steps.includes('process-payment')) {
await ctx.step('process-payment', async () => {
// Payment logic...
});
}
// Continue with remaining steps...
}
Replay from Step
Re-execute starting from a specific step, using state from the original execution:
- TypeScript
- Java
- Go
- .NET
const result = await ductape.features.replayFromStep({
feature_id: 'wf-abc123',
from_step: 'create-shipment',
});
Map<String, Object> result = ductape.features.replayFromStep(Map.of(
"feature_id", "wf-abc123",
"from_step", "create-shipment"
));
result := client.features.replayFromStep({
"feature_id": "wf-abc123",
"from_step": "create-shipment",
});
var result = await ductape.features.replayFromStep({
["feature_id"] = "wf-abc123",
["from_step"] = "create-shipment",
});
Step Replay with State Override
- TypeScript
- Java
- Go
- .NET
// Replay from step with corrected previous step outputs
const result = await ductape.features.replayFromStep({
feature_id: 'wf-abc123',
from_step: 'process-payment',
step_outputs: {
'validate-order': {
valid: true,
total: 150.00, // Corrected total
},
},
});
// Replay from step with corrected previous step outputs
Map<String, Object> result = ductape.features.replayFromStep(Map.of(
"feature_id", "wf-abc123",
"from_step", "process-payment",
step_outputs: Map.of(
'validate-order': Map.of(
"valid", true,
"total", 150.00, // Corrected total
)
)
));
// Replay from step with corrected previous step outputs
result := client.features.replayFromStep({
"feature_id": "wf-abc123",
"from_step": "process-payment",
step_outputs: {
'validate-order': {
"valid": true,
"total": 150.00, // Corrected total
},
},
});
// Replay from step with corrected previous step outputs
var result = await ductape.features.replayFromStep({
["feature_id"] = "wf-abc123",
["from_step"] = "process-payment",
step_outputs: {
'validate-order': {
["valid"] = true,
["total"] = 150.00, // Corrected total
},
},
});
Feature History
Get detailed execution history for debugging and auditing:
Get Execution History
- TypeScript
- Java
- Go
- .NET
const history = await ductape.features.history({
feature_id: 'wf-abc123',
});
console.log(history.events);
// [
// { type: 'feature_started', timestamp: 1701936000000, data: { input: {...} } },
// { type: 'step_started', timestamp: 1701936001000, data: { step: 'validate-order' } },
// { type: 'step_completed', timestamp: 1701936002000, data: { step: 'validate-order', output: {...} } },
// { type: 'checkpoint_created', timestamp: 1701936003000, data: { name: 'validation-complete' } },
// { type: 'step_started', timestamp: 1701936004000, data: { step: 'process-payment' } },
// { type: 'step_failed', timestamp: 1701936005000, data: { step: 'process-payment', error: {...} } },
// { type: 'rollback_started', timestamp: 1701936006000, data: {} },
// { type: 'feature_rolled_back', timestamp: 1701936008000, data: {} },
// ]
Map<String, Object> history = ductape.features.history(Map.of(
"feature_id", "wf-abc123"
));
System.out.println(history.events);
// [
// Map.of( "type", "feature_started", "timestamp", 1701936000000, data: Map.of( input: Map.of(...) ) ),
// Map.of( "type", "step_started", "timestamp", 1701936001000, data: Map.of( "step", "validate-order" ) ),
// Map.of( "type", "step_completed", "timestamp", 1701936002000, data: Map.of( "step", "validate-order", output: Map.of(...) ) ),
// Map.of( "type", "checkpoint_created", "timestamp", 1701936003000, data: Map.of( "name", "validation-complete" ) ),
// Map.of( "type", "step_started", "timestamp", 1701936004000, data: Map.of( "step", "process-payment" ) ),
// Map.of( "type", "step_failed", "timestamp", 1701936005000, data: Map.of( "step", "process-payment", error: Map.of(...) ) ),
// Map.of( "type", "rollback_started", "timestamp", 1701936006000, data: Map.of() ),
// Map.of( "type", "feature_rolled_back", "timestamp", 1701936008000, data: Map.of() ),
// ]
history := client.features.history({
"feature_id": "wf-abc123",
});
fmt.Println(history.events);
// [
// { "type": "feature_started", "timestamp": 1701936000000, data: { input: {...} } },
// { "type": "step_started", "timestamp": 1701936001000, data: { "step": "validate-order" } },
// { "type": "step_completed", "timestamp": 1701936002000, data: { "step": "validate-order", output: {...} } },
// { "type": "checkpoint_created", "timestamp": 1701936003000, data: { "name": "validation-complete" } },
// { "type": "step_started", "timestamp": 1701936004000, data: { "step": "process-payment" } },
// { "type": "step_failed", "timestamp": 1701936005000, data: { "step": "process-payment", error: {...} } },
// { "type": "rollback_started", "timestamp": 1701936006000, data: {} },
// { "type": "feature_rolled_back", "timestamp": 1701936008000, data: {} },
// ]
var history = await ductape.features.history({
["feature_id"] = "wf-abc123",
});
Console.WriteLine(history.events);
// [
// { ["type"] = "feature_started", ["timestamp"] = 1701936000000, data: { input: {...} } },
// { ["type"] = "step_started", ["timestamp"] = 1701936001000, data: { ["step"] = "validate-order" } },
// { ["type"] = "step_completed", ["timestamp"] = 1701936002000, data: { ["step"] = "validate-order", output: {...} } },
// { ["type"] = "checkpoint_created", ["timestamp"] = 1701936003000, data: { ["name"] = "validation-complete" } },
// { ["type"] = "step_started", ["timestamp"] = 1701936004000, data: { ["step"] = "process-payment" } },
// { ["type"] = "step_failed", ["timestamp"] = 1701936005000, data: { ["step"] = "process-payment", error: {...} } },
// { ["type"] = "rollback_started", ["timestamp"] = 1701936006000, data: {} },
// { ["type"] = "feature_rolled_back", ["timestamp"] = 1701936008000, data: {} },
// ]
Get Step Details
- TypeScript
- Java
- Go
- .NET
const stepDetail = await ductape.features.stepDetail({
feature_id: 'wf-abc123',
step_tag: 'process-payment',
});
console.log(stepDetail);
// {
// tag: 'process-payment',
// status: 'failed',
// input: { body: { amount: 100, customer_id: 'CUST-123' } },
// output: null,
// error: { message: 'Card declined', code: 'card_declined' },
// attempts: 3,
// duration: 1000,
// rollback_status: 'completed',
// }
Map<String, Object> stepDetail = ductape.features.stepDetail(Map.of(
"feature_id", "wf-abc123",
"step_tag", "process-payment"
));
System.out.println(stepDetail);
// Map.of(
// "tag", "process-payment",
// "status", "failed",
// input: Map.of( body: Map.of( "amount", 100, "customer_id", "CUST-123" ) ),
// output: null,
// error: Map.of( "message", "Card declined", "code", "card_declined" ),
// "attempts", 3,
// "duration", 1000,
// "rollback_status", "completed",
// )
stepDetail := client.features.stepDetail({
"feature_id": "wf-abc123",
"step_tag": "process-payment",
});
fmt.Println(stepDetail);
// {
// "tag": "process-payment",
// "status": "failed",
// input: { body: { "amount": 100, "customer_id": "CUST-123" } },
// output: null,
// error: { "message": "Card declined", "code": "card_declined" },
// "attempts": 3,
// "duration": 1000,
// "rollback_status": "completed",
// }
var stepDetail = await ductape.features.stepDetail({
["feature_id"] = "wf-abc123",
["step_tag"] = "process-payment",
});
Console.WriteLine(stepDetail);
// {
// ["tag"] = "process-payment",
// ["status"] = "failed",
// input: { body: { ["amount"] = 100, ["customer_id"] = "CUST-123" } },
// output: null,
// error: { ["message"] = "Card declined", ["code"] = "card_declined" },
// ["attempts"] = 3,
// ["duration"] = 1000,
// ["rollback_status"] = "completed",
// }
List Related Executions
Find all replays, restarts, and resumes of a feature:
- TypeScript
- Java
- Go
- .NET
const related = await ductape.features.relatedExecutions({
feature_id: 'wf-abc123',
});
console.log(related.executions);
// [
// { feature_id: 'wf-abc123', type: 'original', status: 'failed' },
// { feature_id: 'wf-def456', type: 'replay', status: 'failed', replayed_from: 'wf-abc123' },
// { feature_id: 'wf-ghi789', type: 'restart', status: 'completed', restarted_from: 'wf-abc123' },
// ]
Map<String, Object> related = ductape.features.relatedExecutions(Map.of(
"feature_id", "wf-abc123"
));
System.out.println(related.executions);
// [
// Map.of( "feature_id", "wf-abc123", "type", "original", "status", "failed" ),
// Map.of( "feature_id", "wf-def456", "type", "replay", "status", "failed", "replayed_from", "wf-abc123" ),
// Map.of( "feature_id", "wf-ghi789", "type", "restart", "status", "completed", "restarted_from", "wf-abc123" ),
// ]
related := client.features.relatedExecutions({
"feature_id": "wf-abc123",
});
fmt.Println(related.executions);
// [
// { "feature_id": "wf-abc123", "type": "original", "status": "failed" },
// { "feature_id": "wf-def456", "type": "replay", "status": "failed", "replayed_from": "wf-abc123" },
// { "feature_id": "wf-ghi789", "type": "restart", "status": "completed", "restarted_from": "wf-abc123" },
// ]
var related = await ductape.features.relatedExecutions({
["feature_id"] = "wf-abc123",
});
Console.WriteLine(related.executions);
// [
// { ["feature_id"] = "wf-abc123", ["type"] = "original", ["status"] = "failed" },
// { ["feature_id"] = "wf-def456", ["type"] = "replay", ["status"] = "failed", ["replayed_from"] = "wf-abc123" },
// { ["feature_id"] = "wf-ghi789", ["type"] = "restart", ["status"] = "completed", ["restarted_from"] = "wf-abc123" },
// ]
Compare Executions
Compare two executions to see what changed:
- TypeScript
- Java
- Go
- .NET
const comparison = await ductape.features.compare({
feature_ids: ['wf-abc123', 'wf-ghi789'],
});
console.log(comparison.input_diff);
// { email: ['wrong@email.com', 'correct@email.com'] }
console.log(comparison.step_diffs);
// [
// {
// step: 'process-payment',
// 'wf-abc123': { status: 'failed', error: 'Card declined' },
// 'wf-ghi789': { status: 'completed', output: { charge_id: 'ch_123' } },
// },
// ]
Map<String, Object> comparison = ductape.features.compare(Map.of(
feature_ids: ['wf-abc123', 'wf-ghi789']
));
System.out.println(comparison.input_diff);
// Map.of( email: ['wrong@email.com', 'correct@email.com'] )
System.out.println(comparison.step_diffs);
// [
// Map.of(
// "step", "process-payment",
// 'wf-abc123': Map.of( "status", "failed", "error", "Card declined" ),
// 'wf-ghi789': Map.of( "status", "completed", output: Map.of( "charge_id", "ch_123" ) ),
// ),
// ]
comparison := client.features.compare({
feature_ids: ['wf-abc123', 'wf-ghi789'],
});
fmt.Println(comparison.input_diff);
// { email: ['wrong@email.com', 'correct@email.com'] }
fmt.Println(comparison.step_diffs);
// [
// {
// "step": "process-payment",
// 'wf-abc123': { "status": "failed", "error": "Card declined" },
// 'wf-ghi789': { "status": "completed", output: { "charge_id": "ch_123" } },
// },
// ]
var comparison = await ductape.features.compare({
feature_ids: ['wf-abc123', 'wf-ghi789'],
});
Console.WriteLine(comparison.input_diff);
// { email: ['wrong@email.com', 'correct@email.com'] }
Console.WriteLine(comparison.step_diffs);
// [
// {
// ["step"] = "process-payment",
// 'wf-abc123': { ["status"] = "failed", ["error"] = "Card declined" },
// 'wf-ghi789': { ["status"] = "completed", output: { ["charge_id"] = "ch_123" } },
// },
// ]
Child Features
Run features as children of other features:
- TypeScript
- Java
- Go
- .NET
handler: async (ctx) => {
// Run payment as a child feature
const paymentResult = await ctx.feature(
'payment-' + ctx.input.orderId, // Child feature ID
'payment-processing', // Feature tag
{ // Input
amount: ctx.input.amount,
customerId: ctx.input.customerId,
},
{
timeout: '5m',
retries: 2,
parent_close_policy: 'terminate', // Cancel child if parent fails
}
);
ctx.log.info('Payment completed', { chargeId: paymentResult.chargeId });
}
handler: async (ctx) => Map.of(
// Run payment as a child feature
Map<String, Object> paymentResult = ctx.feature(
'payment-' + ctx.input.orderId, // Child feature ID
'payment-processing', // Feature tag
Map.of( // Input
amount: ctx.input.amount,
customerId: ctx.input.customerId
),
Map.of(
"timeout", "5m",
"retries", 2,
"parent_close_policy", "terminate", // Cancel child if parent fails
)
);
ctx.log.info('Payment completed', Map.of( chargeId: paymentResult.chargeId ));
)
handler: async (ctx) => {
// Run payment as a child feature
paymentResult := ctx.feature(
'payment-' + ctx.input.orderId, // Child feature ID
'payment-processing', // Feature tag
{ // Input
amount: ctx.input.amount,
customerId: ctx.input.customerId,
},
{
"timeout": "5m",
"retries": 2,
"parent_close_policy": "terminate", // Cancel child if parent fails
}
);
ctx.log.info('Payment completed', { chargeId: paymentResult.chargeId });
}
handler: async (ctx) => {
// Run payment as a child feature
var paymentResult = await ctx.feature(
'payment-' + ctx.input.orderId, // Child feature ID
'payment-processing', // Feature tag
{ // Input
amount: ctx.input.amount,
customerId: ctx.input.customerId,
},
{
["timeout"] = "5m",
["retries"] = 2,
["parent_close_policy"] = "terminate", // Cancel child if parent fails
}
);
ctx.log.info('Payment completed', { chargeId: paymentResult.chargeId });
}
Parent Close Policies
| Policy | Description |
|---|---|
terminate | Cancel child feature when parent completes/fails |
abandon | Let child feature continue independently |
request_cancel | Request cancellation but don't force it |
Error Handling
Try-Catch in Steps
- TypeScript
- Java
- Go
- .NET
await ctx.step('risky-operation', async () => {
try {
return await ctx.api.run({
app: 'unreliable-service',
event: 'fetch',
input: {},
});
} catch (error) {
ctx.log.warn('Primary service failed, using fallback', { error: error.message });
return await ctx.api.run({
app: 'fallback-service',
event: 'fetch',
input: {},
});
}
});
ctx.step('risky-operation', async () => Map.of(
try Map.of(
return ctx.api.run(Map.of(
"app", "unreliable-service",
"event", "fetch",
input: Map.of()
));
) catch (error) Map.of(
ctx.log.warn('Primary service failed, using fallback', Map.of( error: error.message ));
return ctx.api.run(Map.of(
"app", "fallback-service",
"event", "fetch",
input: Map.of()
));
)
));
ctx.step('risky-operation', async () => {
try {
return ctx.api.run({
"app": "unreliable-service",
"event": "fetch",
input: {},
});
} catch (error) {
ctx.log.warn('Primary service failed, using fallback', { error: error.message });
return ctx.api.run({
"app": "fallback-service",
"event": "fetch",
input: {},
});
}
});
await ctx.step('risky-operation', async () => {
try {
return await ctx.api.run({
["app"] = "unreliable-service",
["event"] = "fetch",
input: {},
});
} catch (error) {
ctx.log.warn('Primary service failed, using fallback', { error: error.message });
return await ctx.api.run({
["app"] = "fallback-service",
["event"] = "fetch",
input: {},
});
}
});
Fallback Component
Use the fallback component for structured failure handling:
- TypeScript
- Java
- Go
- .NET
const result = await ctx.fallback.execute({
fallback: 'payment-fallback',
input: {
amount: ctx.input.amount,
method: ctx.input.paymentMethod,
},
timeout: 30000,
});
Map<String, Object> result = ctx.fallback.execute(Map.of(
"fallback", "payment-fallback",
input: Map.of(
amount: ctx.input.amount,
method: ctx.input.paymentMethod
),
"timeout", 30000
));
result := ctx.fallback.execute({
"fallback": "payment-fallback",
input: {
amount: ctx.input.amount,
method: ctx.input.paymentMethod,
},
"timeout": 30000,
});
var result = await ctx.fallback.execute({
["fallback"] = "payment-fallback",
input: {
amount: ctx.input.amount,
method: ctx.input.paymentMethod,
},
["timeout"] = 30000,
});
Best Practices
1. Always Define Rollback Handlers for Critical Operations
- TypeScript
- Java
- Go
- .NET
// Good: Payment has rollback
await ctx.step('payment', chargeCard, refundCard);
// Bad: No way to undo if later steps fail
await ctx.step('payment', chargeCard);
// Good: Payment has rollback
ctx.step('payment', chargeCard, refundCard);
// Bad: No way to undo if later steps fail
ctx.step('payment', chargeCard);
// Good: Payment has rollback
ctx.step('payment', chargeCard, refundCard);
// Bad: No way to undo if later steps fail
ctx.step('payment', chargeCard);
// Good: Payment has rollback
await ctx.step('payment', chargeCard, refundCard);
// Bad: No way to undo if later steps fail
await ctx.step('payment', chargeCard);
2. Use Checkpoints for Long Features
- TypeScript
- Java
- Go
- .NET
// Break long features into phases with checkpoints
await ctx.checkpoint('phase-1-complete');
// ...later steps won't roll back phase 1
// Break long features into phases with checkpoints
ctx.checkpoint('phase-1-complete');
// ...later steps won't roll back phase 1
// Break long features into phases with checkpoints
ctx.checkpoint('phase-1-complete');
// ...later steps won't roll back phase 1
// Break long features into phases with checkpoints
await ctx.checkpoint('phase-1-complete');
// ...later steps won't roll back phase 1
3. Mark Non-Critical Steps with allow_fail
- TypeScript
- Java
- Go
- .NET
// Email failures shouldn't stop order processing
await ctx.step('send-email', sendEmail, null, { allow_fail: true });
// Email failures shouldn't stop order processing
ctx.step('send-email', sendEmail, null, Map.of( "allow_fail", true ));
// Email failures shouldn't stop order processing
ctx.step('send-email', sendEmail, null, { "allow_fail": true });
// Email failures shouldn't stop order processing
await ctx.step('send-email', sendEmail, null, { ["allow_fail"] = true });
4. Use Idempotency Keys for Retries
- TypeScript
- Java
- Go
- .NET
await ctx.api.run({
app: 'stripe',
event: 'charge',
input: {
body: { amount: 1000 },
headers: { 'Idempotency-Key': `${ctx.feature_id}-payment` },
},
});
ctx.api.run(Map.of(
"app", "stripe",
"event", "charge",
input: Map.of(
body: Map.of( "amount", 1000 ),
headers: Map.of( 'Idempotency-Key': `$Map.of(ctx.feature_id)-payment` )
)
));
ctx.api.run({
"app": "stripe",
"event": "charge",
input: {
body: { "amount": 1000 },
headers: { 'Idempotency-Key': `${ctx.feature_id}-payment` },
},
});
await ctx.api.run({
["app"] = "stripe",
["event"] = "charge",
input: {
body: { ["amount"] = 1000 },
headers: { 'Idempotency-Key': `${ctx.feature_id}-payment` },
},
});
5. Log Important State Changes
- TypeScript
- Java
- Go
- .NET
ctx.log.info('Order status changed', {
orderId: order.id,
from: 'pending',
to: 'processing',
});
ctx.log.info('Order status changed', Map.of(
orderId: order.id,
"from", "pending",
"to", "processing"
));
ctx.log.info('Order status changed', {
orderId: order.id,
"from": "pending",
"to": "processing",
});
ctx.log.info('Order status changed', {
orderId: order.id,
["from"] = "pending",
["to"] = "processing",
});
Next Steps
- Examples - Real-world feature patterns
- Building Features - Complete API reference
- Context API - All ctx methods