Building Features
This guide covers the complete code-first API for defining features using ductape.features.define().
Feature Definition
Define a feature by passing a configuration object with a handler function:
- TypeScript
- Java
- Go
- .NET
const feature = await ductape.features.define({
// Target product
tag: 'feature-tag', // Unique identifier
name: 'Feature Name', // Display name
description: 'Description', // Optional
handler: async (ctx) => {
// Your feature logic
return { result: 'data' };
},
});
Map<String, Object> feature = ductape.features.define(Map.of(
// Target product
"tag", "feature-tag", // Unique identifier
"name", "Feature Name", // Display name
"description", "Description", // Optional
handler: async (ctx) => Map.of(
// Your feature logic
return Map.of( "result", "data" );
)
));
feature := client.features.define({
// Target product
"tag": "feature-tag", // Unique identifier
"name": "Feature Name", // Display name
"description": "Description", // Optional
handler: async (ctx) => {
// Your feature logic
return { "result": "data" };
},
});
var feature = await ductape.features.define({
// Target product
["tag"] = "feature-tag", // Unique identifier
["name"] = "Feature Name", // Display name
["description"] = "Description", // Optional
handler: async (ctx) => {
// Your feature logic
return { ["result"] = "data" };
},
});
Definition Options
Basic Properties
- TypeScript
- Java
- Go
- .NET
await ductape.features.define({
tag: 'order-process',
name: 'Order Processing',
description: 'Handles complete order fulfillment',
// Feature options
options: {
timeout: 300000, // 5 minutes max
rollback_strategy: 'reverse_all', // How to handle failures
},
// Environment configurations
envs: [
{ slug: 'dev', active: true },
{ slug: 'staging', active: true },
{ slug: 'prd', active: true },
],
handler: async (ctx) => {
// ...
},
});
ductape.features.define(Map.of(
"tag", "order-process",
"name", "Order Processing",
"description", "Handles complete order fulfillment",
// Feature options
options: Map.of(
"timeout", 300000, // 5 minutes max
"rollback_strategy", "reverse_all", // How to handle failures
),
// Environment configurations
envs: [
Map.of( "slug", "dev", "active", true ),
Map.of( "slug", "staging", "active", true ),
Map.of( "slug", "prd", "active", true ),
],
handler: async (ctx) => Map.of(
// ...
)
));
client.features.define({
"tag": "order-process",
"name": "Order Processing",
"description": "Handles complete order fulfillment",
// Feature options
options: {
"timeout": 300000, // 5 minutes max
"rollback_strategy": "reverse_all", // How to handle failures
},
// Environment configurations
envs: [
{ "slug": "dev", "active": true },
{ "slug": "staging", "active": true },
{ "slug": "prd", "active": true },
],
handler: async (ctx) => {
// ...
},
});
await ductape.features.define({
["tag"] = "order-process",
["name"] = "Order Processing",
["description"] = "Handles complete order fulfillment",
// Feature options
options: {
["timeout"] = 300000, // 5 minutes max
["rollback_strategy"] = "reverse_all", // How to handle failures
},
// Environment configurations
envs: [
{ ["slug"] = "dev", ["active"] = true },
{ ["slug"] = "staging", ["active"] = true },
{ ["slug"] = "prd", ["active"] = true },
],
handler: async (ctx) => {
// ...
},
});
Recording options (conditionals, loops, switch)
When your handler uses if/early return, loops, or switch, the compiler needs help to record all branches. See Conditionals, loops, and switch for full details.
| Option | Purpose |
|---|---|
| branchOverrides | Step result overrides so the handler continues past an if (result.x) return … and records later steps. Those steps get a condition at runtime (e.g. $Step{validate}{valid} == true). |
| recordInput | Sample input used during recording so loops (e.g. for (const item of ctx.input.items)) run and record steps. Use unique step tags per iteration. |
| recordScenarios | Array of input scenarios; the handler runs once per scenario. Each step gets a condition (e.g. $Input{type} == 'a') so only the matching branch runs at runtime. Use for switch-style branches. |
Signals
Define signals that external systems can send to the feature:
- TypeScript
- Java
- Go
- .NET
await ductape.features.define({
// ...
signals: {
'approve': { input: { approver_id: 'string', comments: 'string' } },
'cancel': { input: { reason: 'string' } },
},
handler: async (ctx) => {
// Wait for approval signal
const approval = await ctx.waitForSignal('approve', { timeout: '24h' });
console.log('Approved by:', approval.approver_id);
},
});
ductape.features.define(Map.of(
// ...
signals: Map.of(
'approve': Map.of( input: Map.of( "approver_id", "string", "comments", "string" ) ),
'cancel': Map.of( input: Map.of( "reason", "string" ) )
),
handler: async (ctx) => Map.of(
// Wait for approval signal
Map<String, Object> approval = ctx.waitForSignal('approve', Map.of( "timeout", "24h" ));
System.out.println('Approved by:', approval.approver_id);
)
));
client.features.define({
// ...
signals: {
'approve': { input: { "approver_id": "string", "comments": "string" } },
'cancel': { input: { "reason": "string" } },
},
handler: async (ctx) => {
// Wait for approval signal
approval := ctx.waitForSignal('approve', { "timeout": "24h" });
fmt.Println('Approved by:', approval.approver_id);
},
});
await ductape.features.define({
// ...
signals: {
'approve': { input: { ["approver_id"] = "string", ["comments"] = "string" } },
'cancel': { input: { ["reason"] = "string" } },
},
handler: async (ctx) => {
// Wait for approval signal
var approval = await ctx.waitForSignal('approve', { ["timeout"] = "24h" });
Console.WriteLine('Approved by:', approval.approver_id);
},
});
Queries
Define queries to check feature state:
- TypeScript
- Java
- Go
- .NET
await ductape.features.define({
// ...
queries: {
'getProgress': {
handler: (ctx) => ({
completed_steps: ctx.completed_steps,
current_step: ctx.current_step,
}),
},
},
handler: async (ctx) => {
// ...
},
});
ductape.features.define(Map.of(
// ...
queries: Map.of(
'getProgress': Map.of(
handler: (ctx) => (Map.of(
completed_steps: ctx.completed_steps,
current_step: ctx.current_step
))
)
),
handler: async (ctx) => Map.of(
// ...
)
));
client.features.define({
// ...
queries: {
'getProgress': {
handler: (ctx) => ({
completed_steps: ctx.completed_steps,
current_step: ctx.current_step,
}),
},
},
handler: async (ctx) => {
// ...
},
});
await ductape.features.define({
// ...
queries: {
'getProgress': {
handler: (ctx) => ({
completed_steps: ctx.completed_steps,
current_step: ctx.current_step,
}),
},
},
handler: async (ctx) => {
// ...
},
});
Conditionals, loops, and switch
The handler runs once at define time to record steps. If you use if, early return, loops, or switch, only the path taken during that run is recorded. Use the options below so all branches are recorded and the right step conditions are applied at runtime.
| You want to… | Use | Runtime behavior |
|---|---|---|
| If / else on a step result (e.g. "only run payment if validation passed") | branchOverrides | Steps after the branch get a condition like $Step{validate}{valid} == true; executor skips them when the condition is false. |
Loop over input (e.g. process each item in ctx.input.items) | recordInput | Supply sample input so the loop runs during recording; steps are recorded per iteration. |
If/else or switch on feature input (e.g. different steps for type: 'a' vs type: 'b') | recordScenarios | Handler runs once per scenario; each step gets a condition like $Input{type} == 'a' and only runs when input matches. |
If / else (early return) – branchOverrides
When the handler branches on a step result and returns early, later steps are never recorded. Use branchOverrides so the compiler "pretends" a step returned a value that keeps the handler running and records those steps. At runtime, those steps get a condition so they only run when the real step output matches.
- TypeScript
- Java
- Go
- .NET
await ductape.features.define({
tag: 'order-fulfillment',
name: 'Order Fulfillment',
// So the handler continues past the if and records process-payment
branchOverrides: {
validate: { valid: true },
},
handler: async (ctx) => {
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 };
const payment = await ctx.step('process-payment', async () =>
ctx.api.run({
app: 'stripe',
event: 'create-charge',
input: { body: { amount: ctx.input.amount } },
})
);
return { success: true, orderId: ctx.input.orderId, chargeId: payment.id };
},
});
ductape.features.define(Map.of(
"tag", "order-fulfillment",
"name", "Order Fulfillment",
// So the handler continues past the if and records process-payment
branchOverrides: Map.of(
validate: Map.of( "valid", true )
),
handler: async (ctx) => Map.of(
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) return Map.of( "success", false, error: validation.reason );
Map<String, Object> payment = ctx.step('process-payment', async () =>
ctx.api.run(Map.of(
"app", "stripe",
"event", "create-charge",
input: Map.of( body: Map.of( amount: ctx.input.amount ) )
))
);
return Map.of( "success", true, orderId: ctx.input.orderId, chargeId: payment.id );
)
));
client.features.define({
"tag": "order-fulfillment",
"name": "Order Fulfillment",
// So the handler continues past the if and records process-payment
branchOverrides: {
validate: { "valid": true },
},
handler: async (ctx) => {
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 };
payment := ctx.step('process-payment', async () =>
ctx.api.run({
"app": "stripe",
"event": "create-charge",
input: { body: { amount: ctx.input.amount } },
})
);
return { "success": true, orderId: ctx.input.orderId, chargeId: payment.id };
},
});
await ductape.features.define({
["tag"] = "order-fulfillment",
["name"] = "Order Fulfillment",
// So the handler continues past the if and records process-payment
branchOverrides: {
validate: { ["valid"] = true },
},
handler: async (ctx) => {
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 };
var payment = await ctx.step('process-payment', async () =>
ctx.api.run({
["app"] = "stripe",
["event"] = "create-charge",
input: { body: { amount: ctx.input.amount } },
})
);
return { ["success"] = true, orderId: ctx.input.orderId, chargeId: payment.id };
},
});
- Compile: With
validate: { valid: true }, the handler does not early-return, soprocess-paymentis recorded. The step gets condition$Step{validate}{valid} == true. - Runtime: The executor runs
validate; if the real result hasvalid === false,process-paymentis skipped; ifvalid === true, it runs.
Loops (for) – recordInput
By default, recording uses empty input, so a loop like for (const item of ctx.input.items) runs zero times and no steps are recorded. Use recordInput to supply sample input during recording so the loop runs and each iteration’s steps are recorded.
Use unique step tags per iteration (e.g. `process-${item.id}`).
- TypeScript
- Java
- Go
- .NET
await ductape.features.define({
tag: 'process-orders',
name: 'Process Orders',
recordInput: {
items: [{ id: '1' }, { id: '2' }],
},
handler: async (ctx) => {
for (const item of ctx.input.items) {
await ctx.step(`process-${item.id}`, async () =>
ctx.api.run({
app: 'orders-api',
event: 'process-item',
input: { body: { id: item.id } },
})
);
}
},
});
ductape.features.define(Map.of(
"tag", "process-orders",
"name", "Process Orders",
recordInput: Map.of(
items: [Map.of( "id", "1" ), Map.of( "id", "2" )]
),
handler: async (ctx) => Map.of(
for (Map<String, Object> item of ctx.input.items) Map.of(
ctx.step(`process-$Map.of(item.id)`, async () =>
ctx.api.run(Map.of(
"app", "orders-api",
"event", "process-item",
input: Map.of( body: Map.of( id: item.id ) )
))
);
)
)
));
client.features.define({
"tag": "process-orders",
"name": "Process Orders",
recordInput: {
items: [{ "id": "1" }, { "id": "2" }],
},
handler: async (ctx) => {
for (const item of ctx.input.items) {
ctx.step(`process-${item.id}`, async () =>
ctx.api.run({
"app": "orders-api",
"event": "process-item",
input: { body: { id: item.id } },
})
);
}
},
});
await ductape.features.define({
["tag"] = "process-orders",
["name"] = "Process Orders",
recordInput: {
items: [{ ["id"] = "1" }, { ["id"] = "2" }],
},
handler: async (ctx) => {
for (var item of ctx.input.items) {
await ctx.step(`process-${item.id}`, async () =>
ctx.api.run({
["app"] = "orders-api",
["event"] = "process-item",
input: { body: { id: item.id } },
})
);
}
},
});
Recording runs with ctx.input.items = [{ id: '1' }, { id: '2' }], so steps process-1 and process-2 are recorded. Keys not in recordInput still compile to $Input{...} in the schema.
If/else or switch on input – recordScenarios
For if/else or switch on feature input (e.g. different steps per type or status), use recordScenarios. The handler runs once per scenario (with that scenario as input). Every step recorded in that run gets a condition so it only runs when the real input matches (e.g. $Input{type} == 'a'). Steps from all runs are merged; if the same step appears in multiple scenarios, conditions are combined with ||. You can use either if (ctx.input.type === 'a') { ... } else if (ctx.input.type === 'b') { ... } or switch (ctx.input.type) { case 'a': ... } in the handler.
- TypeScript
- Java
- Go
- .NET
await ductape.features.define({
tag: 'handle-by-type',
name: 'Handle by type',
recordScenarios: [
{ type: 'a' },
{ type: 'b' },
],
handler: async (ctx) => {
switch (ctx.input.type) {
case 'a':
await ctx.step('handle-a', async () =>
ctx.api.run({ app: 'api', event: 'handle-a', input: {} })
);
break;
case 'b':
await ctx.step('handle-b', async () =>
ctx.api.run({ app: 'api', event: 'handle-b', input: {} })
);
break;
}
},
});
ductape.features.define(Map.of(
"tag", "handle-by-type",
"name", "Handle by type",
recordScenarios: [
Map.of( "type", "a" ),
Map.of( "type", "b" ),
],
handler: async (ctx) => Map.of(
switch (ctx.input.type) Map.of(
case 'a':
ctx.step('handle-a', async () =>
ctx.api.run(Map.of( "app", "api", "event", "handle-a", input: Map.of() ))
);
break;
case 'b':
ctx.step('handle-b', async () =>
ctx.api.run(Map.of( "app", "api", "event", "handle-b", input: Map.of() ))
);
break;
)
)
));
client.features.define({
"tag": "handle-by-type",
"name": "Handle by type",
recordScenarios: [
{ "type": "a" },
{ "type": "b" },
],
handler: async (ctx) => {
switch (ctx.input.type) {
case 'a':
ctx.step('handle-a', async () =>
ctx.api.run({ "app": "api", "event": "handle-a", input: {} })
);
break;
case 'b':
ctx.step('handle-b', async () =>
ctx.api.run({ "app": "api", "event": "handle-b", input: {} })
);
break;
}
},
});
await ductape.features.define({
["tag"] = "handle-by-type",
["name"] = "Handle by type",
recordScenarios: [
{ ["type"] = "a" },
{ ["type"] = "b" },
],
handler: async (ctx) => {
switch (ctx.input.type) {
case 'a':
await ctx.step('handle-a', async () =>
ctx.api.run({ ["app"] = "api", ["event"] = "handle-a", input: {} })
);
break;
case 'b':
await ctx.step('handle-b', async () =>
ctx.api.run({ ["app"] = "api", ["event"] = "handle-b", input: {} })
);
break;
}
},
});
- Compile: Two runs: one with
{ type: 'a' }, one with{ type: 'b' }. Stepshandle-a(condition$Input{type} == 'a') andhandle-b(condition$Input{type} == 'b') are recorded. - Runtime: The executor evaluates the condition and runs only the matching step.
Step condition syntax
Conditions are evaluated at runtime by the feature executor. You can combine conditions with && (and) and || (or). Each part can use these operators:
| Operator | Meaning | Example |
|---|---|---|
==, === | Equal | $Step{validate}{valid} == true |
!=, !== | Not equal | $Input{status} != 'draft' |
>, >= | Greater than (or equal) | $Step{score}{value} >= 5 |
<, <= | Less than (or equal) | $Input{count} <= 100 |
Left and right sides can be:
- Step output:
$Step{stepTag}{field}or$Step{stepTag}{nested.field} - Feature input:
$Input{field}or$Input{nested.field} - Literals:
true,false, numbers, or quoted strings ('a',"draft")
Examples:
$Step{validate}{available} == true
$Input{type} == 'premium'
$Step{score}{value} >= 80 && $Input{role} != 'guest'
($Input{region} == 'eu') || ($Input{region} == 'uk')
Working with Steps
The ctx.step() Function
Steps are the building blocks of features:
- TypeScript
- Java
- Go
- .NET
const result = await ctx.step(
'step-tag', // Unique identifier
handler, // Async function
rollback?, // Optional rollback function
options? // Optional step options
);
Map<String, Object> result = ctx.step(
'step-tag', // Unique identifier
handler, // Async function
rollback?, // Optional rollback function
options? // Optional step options
);
result := ctx.step(
'step-tag', // Unique identifier
handler, // Async function
rollback?, // Optional rollback function
options? // Optional step options
);
var result = await ctx.step(
'step-tag', // Unique identifier
handler, // Async function
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 },
});
});
// Use the result in subsequent steps
console.log('Created user:', user.id);
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 )
));
));
// Use the result in subsequent steps
System.out.println('Created user:', user.id);
user := ctx.step('create-user', async () => {
return ctx.database.insert({
"database": "users-db",
"event": "create-user",
data: { email: ctx.input.email },
});
});
// Use the result in subsequent steps
fmt.Println('Created user:', user.id);
var user = await ctx.step('create-user', async () => {
return ctx.database.insert({
["database"] = "users-db",
["event"] = "create-user",
data: { email: ctx.input.email },
});
});
// Use the result in subsequent steps
Console.WriteLine('Created user:', user.id);
Step with Rollback
- TypeScript
- Java
- Go
- .NET
const charge = await ctx.step(
'charge-card',
// Handler
async () => {
return ctx.api.run({
app: 'stripe',
event: 'create-charge',
input: { body: { amount: ctx.input.amount } },
});
},
// Rollback - called if a later step fails
async (result) => {
await ctx.api.run({
app: 'stripe',
event: 'refund',
input: { body: { chargeId: result.id } },
});
}
);
Map<String, Object> charge = ctx.step(
'charge-card',
// Handler
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 - called if a later step fails
async (result) => Map.of(
ctx.api.run(Map.of(
"app", "stripe",
"event", "refund",
input: Map.of( body: Map.of( chargeId: result.id ) )
));
)
);
charge := ctx.step(
'charge-card',
// Handler
async () => {
return ctx.api.run({
"app": "stripe",
"event": "create-charge",
input: { body: { amount: ctx.input.amount } },
});
},
// Rollback - called if a later step fails
async (result) => {
ctx.api.run({
"app": "stripe",
"event": "refund",
input: { body: { chargeId: result.id } },
});
}
);
var charge = await ctx.step(
'charge-card',
// Handler
async () => {
return ctx.api.run({
["app"] = "stripe",
["event"] = "create-charge",
input: { body: { amount: ctx.input.amount } },
});
},
// Rollback - called if a later step fails
async (result) => {
await ctx.api.run({
["app"] = "stripe",
["event"] = "refund",
input: { body: { chargeId: result.id } },
});
}
);
Step Options
- TypeScript
- Java
- Go
- .NET
await ctx.step(
'send-notification',
async () => {
await ctx.notification.email({ /* ... */ });
},
null, // No rollback
{
allow_fail: true, // Continue on failure
retries: 3, // Retry count
retry_delay: 1000, // Delay between retries (ms)
timeout: 30000, // Step timeout (ms)
critical: false, // Mark as critical for rollback
}
);
ctx.step(
'send-notification',
async () => Map.of(
ctx.notification.email(Map.of( /* ... */ ));
),
null, // No rollback
Map.of(
"allow_fail", true, // Continue on failure
"retries", 3, // Retry count
"retry_delay", 1000, // Delay between retries (ms)
"timeout", 30000, // Step timeout (ms)
"critical", false, // Mark as critical for rollback
)
);
ctx.step(
'send-notification',
async () => {
ctx.notification.email({ /* ... */ });
},
null, // No rollback
{
"allow_fail": true, // Continue on failure
"retries": 3, // Retry count
"retry_delay": 1000, // Delay between retries (ms)
"timeout": 30000, // Step timeout (ms)
"critical": false, // Mark as critical for rollback
}
);
await ctx.step(
'send-notification',
async () => {
await ctx.notification.email({ /* ... */ });
},
null, // No rollback
{
["allow_fail"] = true, // Continue on failure
["retries"] = 3, // Retry count
["retry_delay"] = 1000, // Delay between retries (ms)
["timeout"] = 30000, // Step timeout (ms)
["critical"] = false, // Mark as critical for rollback
}
);
Using Ductape Components
ctx.action - API Calls
Call external APIs through connected apps:
- TypeScript
- Java
- Go
- .NET
// Basic call
const result = await ctx.api.run({
app: 'stripe',
event: 'create-charge',
input: {
body: { amount: 1000, currency: 'usd' },
},
});
// With retries and timeout
const result = await ctx.api.run({
app: 'external-api',
event: 'fetch-data',
input: {
body: { query: ctx.input.query },
headers: { 'X-Request-ID': ctx.feature_id },
},
retries: 3,
timeout: 10000,
});
// Basic call
Map<String, Object> result = ctx.api.run(Map.of(
"app", "stripe",
"event", "create-charge",
input: Map.of(
body: Map.of( "amount", 1000, "currency", "usd" )
)
));
// With retries and timeout
Map<String, Object> result = ctx.api.run(Map.of(
"app", "external-api",
"event", "fetch-data",
input: Map.of(
body: Map.of( query: ctx.input.query ),
headers: Map.of( 'X-Request-ID': ctx.feature_id )
),
"retries", 3,
"timeout", 10000
));
// Basic call
result := ctx.api.run({
"app": "stripe",
"event": "create-charge",
input: {
body: { "amount": 1000, "currency": "usd" },
},
});
// With retries and timeout
result := ctx.api.run({
"app": "external-api",
"event": "fetch-data",
input: {
body: { query: ctx.input.query },
headers: { 'X-Request-ID': ctx.feature_id },
},
"retries": 3,
"timeout": 10000,
});
// Basic call
var result = await ctx.api.run({
["app"] = "stripe",
["event"] = "create-charge",
input: {
body: { ["amount"] = 1000, ["currency"] = "usd" },
},
});
// With retries and timeout
var result = await ctx.api.run({
["app"] = "external-api",
["event"] = "fetch-data",
input: {
body: { query: ctx.input.query },
headers: { 'X-Request-ID': ctx.feature_id },
},
["retries"] = 3,
["timeout"] = 10000,
});
ctx.database - Database Operations
- TypeScript
- Java
- Go
- .NET
// Insert
const record = await ctx.database.insert({
database: 'orders-db',
event: 'create-order',
data: {
customer_id: ctx.input.customerId,
total: ctx.input.amount,
items: ctx.input.items,
},
});
// Query
const orders = await ctx.database.query({
database: 'orders-db',
event: 'find-by-customer',
params: { customerId: ctx.input.customerId },
});
// Execute (for custom operations)
const result = await ctx.database.execute({
database: 'analytics-db',
event: 'aggregate-sales',
input: { startDate: ctx.input.from, endDate: ctx.input.to },
});
// Update
await ctx.database.update({
database: 'orders-db',
event: 'update-status',
where: { id: ctx.input.orderId },
data: { status: 'shipped' },
});
// Delete
await ctx.database.delete({
database: 'orders-db',
event: 'remove-order',
where: { id: ctx.input.orderId },
});
// Insert
Map<String, Object> record = ctx.database.insert(Map.of(
"database", "orders-db",
"event", "create-order",
data: Map.of(
customer_id: ctx.input.customerId,
total: ctx.input.amount,
items: ctx.input.items
)
));
// Query
Map<String, Object> orders = ctx.database.query(Map.of(
"database", "orders-db",
"event", "find-by-customer",
params: Map.of( customerId: ctx.input.customerId )
));
// Execute (for custom operations)
Map<String, Object> result = ctx.database.execute(Map.of(
"database", "analytics-db",
"event", "aggregate-sales",
input: Map.of( startDate: ctx.input.from, endDate: ctx.input.to )
));
// Update
ctx.database.update(Map.of(
"database", "orders-db",
"event", "update-status",
where: Map.of( id: ctx.input.orderId ),
data: Map.of( "status", "shipped" )
));
// Delete
ctx.database.delete(Map.of(
"database", "orders-db",
"event", "remove-order",
where: Map.of( id: ctx.input.orderId )
));
// Insert
record := ctx.database.insert({
"database": "orders-db",
"event": "create-order",
data: {
customer_id: ctx.input.customerId,
total: ctx.input.amount,
items: ctx.input.items,
},
});
// Query
orders := ctx.database.query({
"database": "orders-db",
"event": "find-by-customer",
params: { customerId: ctx.input.customerId },
});
// Execute (for custom operations)
result := ctx.database.execute({
"database": "analytics-db",
"event": "aggregate-sales",
input: { startDate: ctx.input.from, endDate: ctx.input.to },
});
// Update
ctx.database.update({
"database": "orders-db",
"event": "update-status",
where: { id: ctx.input.orderId },
data: { "status": "shipped" },
});
// Delete
ctx.database.delete({
"database": "orders-db",
"event": "remove-order",
where: { id: ctx.input.orderId },
});
// Insert
var record = await ctx.database.insert({
["database"] = "orders-db",
["event"] = "create-order",
data: {
customer_id: ctx.input.customerId,
total: ctx.input.amount,
items: ctx.input.items,
},
});
// Query
var orders = await ctx.database.query({
["database"] = "orders-db",
["event"] = "find-by-customer",
params: { customerId: ctx.input.customerId },
});
// Execute (for custom operations)
var result = await ctx.database.execute({
["database"] = "analytics-db",
["event"] = "aggregate-sales",
input: { startDate: ctx.input.from, endDate: ctx.input.to },
});
// Update
await ctx.database.update({
["database"] = "orders-db",
["event"] = "update-status",
where: { id: ctx.input.orderId },
data: { ["status"] = "shipped" },
});
// Delete
await ctx.database.delete({
["database"] = "orders-db",
["event"] = "remove-order",
where: { id: ctx.input.orderId },
});
ctx.graph - Graph Database
- TypeScript
- Java
- Go
- .NET
// Create node
const node = await ctx.graph.createNode({
graph: 'social-graph',
labels: ['User'],
properties: { name: ctx.input.name, email: ctx.input.email },
});
// Update node
await ctx.graph.updateNode({
graph: 'social-graph',
id: node.id,
properties: { verified: true },
});
// Create relationship
await ctx.graph.createRelationship({
graph: 'social-graph',
from: ctx.input.userId,
to: ctx.input.friendId,
type: 'FOLLOWS',
properties: { since: new Date().toISOString() },
});
// Query
const connections = await ctx.graph.query({
graph: 'social-graph',
action: 'find-connections',
params: { userId: ctx.input.userId, depth: 2 },
});
// Execute custom action
const result = await ctx.graph.execute({
graph: 'social-graph',
action: 'compute-influence',
input: { userId: ctx.input.userId },
});
// Delete node
await ctx.graph.deleteNode({
graph: 'social-graph',
id: ctx.input.nodeId,
});
// Delete relationship
await ctx.graph.deleteRelationship({
graph: 'social-graph',
id: ctx.input.relationshipId,
});
// Create node
Map<String, Object> node = ctx.graph.createNode(Map.of(
"graph", "social-graph",
labels: ['User'],
properties: Map.of( name: ctx.input.name, email: ctx.input.email )
));
// Update node
ctx.graph.updateNode(Map.of(
"graph", "social-graph",
id: node.id,
properties: Map.of( "verified", true )
));
// Create relationship
ctx.graph.createRelationship(Map.of(
"graph", "social-graph",
from: ctx.input.userId,
to: ctx.input.friendId,
"type", "FOLLOWS",
properties: Map.of( since: Instant.now().toISOString() )
));
// Query
Map<String, Object> connections = ctx.graph.query(Map.of(
"graph", "social-graph",
"action", "find-connections",
params: Map.of( userId: ctx.input.userId, "depth", 2 )
));
// Execute custom action
Map<String, Object> result = ctx.graph.execute(Map.of(
"graph", "social-graph",
"action", "compute-influence",
input: Map.of( userId: ctx.input.userId )
));
// Delete node
ctx.graph.deleteNode(Map.of(
"graph", "social-graph",
id: ctx.input.nodeId
));
// Delete relationship
ctx.graph.deleteRelationship(Map.of(
"graph", "social-graph",
id: ctx.input.relationshipId
));
// Create node
node := ctx.graph.createNode({
"graph": "social-graph",
labels: ['User'],
properties: { name: ctx.input.name, email: ctx.input.email },
});
// Update node
ctx.graph.updateNode({
"graph": "social-graph",
id: node.id,
properties: { "verified": true },
});
// Create relationship
ctx.graph.createRelationship({
"graph": "social-graph",
from: ctx.input.userId,
to: ctx.input.friendId,
"type": "FOLLOWS",
properties: { since: new Date().toISOString() },
});
// Query
connections := ctx.graph.query({
"graph": "social-graph",
"action": "find-connections",
params: { userId: ctx.input.userId, "depth": 2 },
});
// Execute custom action
result := ctx.graph.execute({
"graph": "social-graph",
"action": "compute-influence",
input: { userId: ctx.input.userId },
});
// Delete node
ctx.graph.deleteNode({
"graph": "social-graph",
id: ctx.input.nodeId,
});
// Delete relationship
ctx.graph.deleteRelationship({
"graph": "social-graph",
id: ctx.input.relationshipId,
});
// Create node
var node = await ctx.graph.createNode({
["graph"] = "social-graph",
labels: ['User'],
properties: { name: ctx.input.name, email: ctx.input.email },
});
// Update node
await ctx.graph.updateNode({
["graph"] = "social-graph",
id: node.id,
properties: { ["verified"] = true },
});
// Create relationship
await ctx.graph.createRelationship({
["graph"] = "social-graph",
from: ctx.input.userId,
to: ctx.input.friendId,
["type"] = "FOLLOWS",
properties: { since: DateTime.UtcNow.toISOString() },
});
// Query
var connections = await ctx.graph.query({
["graph"] = "social-graph",
["action"] = "find-connections",
params: { userId: ctx.input.userId, ["depth"] = 2 },
});
// Execute custom action
var result = await ctx.graph.execute({
["graph"] = "social-graph",
["action"] = "compute-influence",
input: { userId: ctx.input.userId },
});
// Delete node
await ctx.graph.deleteNode({
["graph"] = "social-graph",
id: ctx.input.nodeId,
});
// Delete relationship
await ctx.graph.deleteRelationship({
["graph"] = "social-graph",
id: ctx.input.relationshipId,
});
ctx.notification - Send Notifications
- TypeScript
- Java
- Go
- .NET
// Email
await ctx.notification.email({
notification: 'transactional',
event: 'order-confirmation',
recipients: [ctx.input.email],
subject: { orderId: ctx.input.orderId },
template: {
orderId: ctx.input.orderId,
items: ctx.input.items,
total: ctx.input.total,
},
});
// SMS
await ctx.notification.sms({
notification: 'alerts',
event: 'verification-code',
phones: [ctx.input.phone],
message: { code: ctx.input.code },
});
// Push notification
await ctx.notification.push({
notification: 'mobile-app',
event: 'order-shipped',
tokens: [ctx.input.deviceToken],
title: { status: 'Shipped' },
body: { trackingNumber: ctx.input.trackingNumber },
data: { orderId: ctx.input.orderId },
});
// Generic send
await ctx.notification.send({
notification: 'multi-channel',
event: 'urgent-alert',
input: {
email: { recipients: [ctx.input.email] },
sms: { phones: [ctx.input.phone] },
push: { tokens: [ctx.input.token] },
},
retries: 3,
});
// Email
ctx.notification.email(Map.of(
"notification", "transactional",
"event", "order-confirmation",
recipients: [ctx.input.email],
subject: Map.of( orderId: ctx.input.orderId ),
template: Map.of(
orderId: ctx.input.orderId,
items: ctx.input.items,
total: ctx.input.total
)
));
// SMS
ctx.notification.sms(Map.of(
"notification", "alerts",
"event", "verification-code",
phones: [ctx.input.phone],
message: Map.of( code: ctx.input.code )
));
// Push notification
ctx.notification.push(Map.of(
"notification", "mobile-app",
"event", "order-shipped",
tokens: [ctx.input.deviceToken],
title: Map.of( "status", "Shipped" ),
body: Map.of( trackingNumber: ctx.input.trackingNumber ),
data: Map.of( orderId: ctx.input.orderId )
));
// Generic send
ctx.notification.send(Map.of(
"notification", "multi-channel",
"event", "urgent-alert",
input: Map.of(
email: Map.of( recipients: [ctx.input.email] ),
sms: Map.of( phones: [ctx.input.phone] ),
push: Map.of( tokens: [ctx.input.token] )
),
"retries", 3
));
// Email
ctx.notification.email({
"notification": "transactional",
"event": "order-confirmation",
recipients: [ctx.input.email],
subject: { orderId: ctx.input.orderId },
template: {
orderId: ctx.input.orderId,
items: ctx.input.items,
total: ctx.input.total,
},
});
// SMS
ctx.notification.sms({
"notification": "alerts",
"event": "verification-code",
phones: [ctx.input.phone],
message: { code: ctx.input.code },
});
// Push notification
ctx.notification.push({
"notification": "mobile-app",
"event": "order-shipped",
tokens: [ctx.input.deviceToken],
title: { "status": "Shipped" },
body: { trackingNumber: ctx.input.trackingNumber },
data: { orderId: ctx.input.orderId },
});
// Generic send
ctx.notification.send({
"notification": "multi-channel",
"event": "urgent-alert",
input: {
email: { recipients: [ctx.input.email] },
sms: { phones: [ctx.input.phone] },
push: { tokens: [ctx.input.token] },
},
"retries": 3,
});
// Email
await ctx.notification.email({
["notification"] = "transactional",
["event"] = "order-confirmation",
recipients: [ctx.input.email],
subject: { orderId: ctx.input.orderId },
template: {
orderId: ctx.input.orderId,
items: ctx.input.items,
total: ctx.input.total,
},
});
// SMS
await ctx.notification.sms({
["notification"] = "alerts",
["event"] = "verification-code",
phones: [ctx.input.phone],
message: { code: ctx.input.code },
});
// Push notification
await ctx.notification.push({
["notification"] = "mobile-app",
["event"] = "order-shipped",
tokens: [ctx.input.deviceToken],
title: { ["status"] = "Shipped" },
body: { trackingNumber: ctx.input.trackingNumber },
data: { orderId: ctx.input.orderId },
});
// Generic send
await ctx.notification.send({
["notification"] = "multi-channel",
["event"] = "urgent-alert",
input: {
email: { recipients: [ctx.input.email] },
sms: { phones: [ctx.input.phone] },
push: { tokens: [ctx.input.token] },
},
["retries"] = 3,
});
ctx.storage - File Operations
- TypeScript
- Java
- Go
- .NET
// Upload file
const uploaded = await ctx.storage.upload({
storage: 'documents',
event: 'upload-invoice',
input: {
buffer: ctx.input.fileData,
fileName: `invoice-${ctx.input.orderId}.pdf`,
mimeType: 'application/pdf',
},
retries: 2,
});
console.log('File URL:', uploaded.url);
console.log('File key:', uploaded.file_key);
// Download file
const file = await ctx.storage.download({
storage: 'templates',
event: 'get-template',
input: { file_key: 'invoice-template.pdf' },
});
console.log('Content:', file.content);
console.log('Size:', file.size);
// Delete file
await ctx.storage.delete({
storage: 'temp-files',
event: 'cleanup',
input: { file_key: ctx.input.tempFileKey },
});
// Upload file
Map<String, Object> uploaded = ctx.storage.upload(Map.of(
"storage", "documents",
"event", "upload-invoice",
input: Map.of(
buffer: ctx.input.fileData,
fileName: `invoice-$Map.of(ctx.input.orderId).pdf`,
"mimeType", "application/pdf"
),
"retries", 2
));
System.out.println('File "URL", ", uploaded.url);
System.out.println("File "key", ", uploaded.file_key);
// Download file
Map<String, Object> file = ctx.storage.download(Map.of(
"storage", "templates',
"event", "get-template",
input: Map.of( "file_key", "invoice-template.pdf" )
));
System.out.println('"Content", ", file.content);
System.out.println(""Size", ", file.size);
// Delete file
ctx.storage.delete(Map.of(
"storage", "temp-files',
"event", "cleanup",
input: Map.of( file_key: ctx.input.tempFileKey )
));
// Upload file
uploaded := ctx.storage.upload({
"storage": "documents",
"event": "upload-invoice",
input: {
buffer: ctx.input.fileData,
fileName: `invoice-${ctx.input.orderId}.pdf`,
"mimeType": "application/pdf",
},
"retries": 2,
});
fmt.Println('File "URL": ", uploaded.url);
fmt.Println("File "key": ", uploaded.file_key);
// Download file
file := ctx.storage.download({
"storage": "templates',
"event": "get-template",
input: { "file_key": "invoice-template.pdf" },
});
fmt.Println('"Content": ", file.content);
fmt.Println(""Size": ", file.size);
// Delete file
ctx.storage.delete({
"storage": "temp-files',
"event": "cleanup",
input: { file_key: ctx.input.tempFileKey },
});
// Upload file
var uploaded = await ctx.storage.upload({
["storage"] = "documents",
["event"] = "upload-invoice",
input: {
buffer: ctx.input.fileData,
fileName: `invoice-${ctx.input.orderId}.pdf`,
["mimeType"] = "application/pdf",
},
["retries"] = 2,
});
Console.WriteLine('File ["URL"] = ", uploaded.url);
Console.WriteLine("File ["key"] = ", uploaded.file_key);
// Download file
var file = await ctx.storage.download({
["storage"] = "templates',
["event"] = "get-template",
input: { ["file_key"] = "invoice-template.pdf" },
});
Console.WriteLine('["Content"] = ", file.content);
Console.WriteLine("["Size"] = ", file.size);
// Delete file
await ctx.storage.delete({
["storage"] = "temp-files',
["event"] = "cleanup",
input: { file_key: ctx.input.tempFileKey },
});
ctx.messaging - Message Broker (Ductape primitive)
- TypeScript
- Java
- Go
- .NET
// Publish message (event = "broker-tag:topic-tag")
await ctx.messaging.produce({
event: 'order-events:order-created',
message: {
orderId: ctx.input.orderId,
customerId: ctx.input.customerId,
items: ctx.input.items,
timestamp: Date.now(),
},
});
// Publish message (event = "broker-tag:topic-tag")
ctx.messaging.produce(Map.of(
"event", "order-events:order-created",
message: Map.of(
orderId: ctx.input.orderId,
customerId: ctx.input.customerId,
items: ctx.input.items,
timestamp: Date.now()
)
));
// Publish message (event = "broker-tag:topic-tag")
ctx.messaging.produce({
"event": "order-events:order-created",
message: {
orderId: ctx.input.orderId,
customerId: ctx.input.customerId,
items: ctx.input.items,
timestamp: Date.now(),
},
});
// Publish message (event = "broker-tag:topic-tag")
await ctx.messaging.produce({
["event"] = "order-events:order-created",
message: {
orderId: ctx.input.orderId,
customerId: ctx.input.customerId,
items: ctx.input.items,
timestamp: Date.now(),
},
});
ctx.quota - Rate Limiting
- TypeScript
- Java
- Go
- .NET
// Check quota before proceeding
const quotaResult = await ctx.quota.execute({
quota: 'api-rate-limit',
input: { key: ctx.input.userId },
timeout: 5000,
});
if (!quotaResult.allowed) {
throw new Error('Rate limit exceeded');
}
// Check quota before proceeding
Map<String, Object> quotaResult = ctx.quota.execute(Map.of(
"quota", "api-rate-limit",
input: Map.of( key: ctx.input.userId ),
"timeout", 5000
));
if (!quotaResult.allowed) Map.of(
throw new Error('Rate limit exceeded');
)
// Check quota before proceeding
quotaResult := ctx.quota.execute({
"quota": "api-rate-limit",
input: { key: ctx.input.userId },
"timeout": 5000,
});
if (!quotaResult.allowed) {
throw new Error('Rate limit exceeded');
}
// Check quota before proceeding
var quotaResult = await ctx.quota.execute({
["quota"] = "api-rate-limit",
input: { key: ctx.input.userId },
["timeout"] = 5000,
});
if (!quotaResult.allowed) {
throw new Error('Rate limit exceeded');
}
ctx.fallback - Failure Handling
- TypeScript
- Java
- Go
- .NET
// Use fallback for resilient operations
const result = await ctx.fallback.execute({
fallback: 'payment-fallback',
input: {
amount: ctx.input.amount,
method: ctx.input.paymentMethod,
},
timeout: 30000,
});
// Use fallback for resilient operations
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
));
// Use fallback for resilient operations
result := ctx.fallback.execute({
"fallback": "payment-fallback",
input: {
amount: ctx.input.amount,
method: ctx.input.paymentMethod,
},
"timeout": 30000,
});
// Use fallback for resilient operations
var result = await ctx.fallback.execute({
["fallback"] = "payment-fallback",
input: {
amount: ctx.input.amount,
method: ctx.input.paymentMethod,
},
["timeout"] = 30000,
});
Control Flow
ctx.sleep() - Pause Execution
- TypeScript
- Java
- Go
- .NET
// Wait 5 seconds
await ctx.sleep(5000);
// Wait using duration string
await ctx.sleep('5m'); // 5 minutes
await ctx.sleep('1h'); // 1 hour
await ctx.sleep('1d'); // 1 day
// Wait 5 seconds
ctx.sleep(5000);
// Wait using duration string
ctx.sleep('5m'); // 5 minutes
ctx.sleep('1h'); // 1 hour
ctx.sleep('1d'); // 1 day
// Wait 5 seconds
ctx.sleep(5000);
// Wait using duration string
ctx.sleep('5m'); // 5 minutes
ctx.sleep('1h'); // 1 hour
ctx.sleep('1d'); // 1 day
// Wait 5 seconds
await ctx.sleep(5000);
// Wait using duration string
await ctx.sleep('5m'); // 5 minutes
await ctx.sleep('1h'); // 1 hour
await ctx.sleep('1d'); // 1 day
ctx.waitForSignal() - Wait for External Event
- TypeScript
- Java
- Go
- .NET
// Wait for a single signal
const approval = await ctx.waitForSignal('order-approved', {
timeout: '24h',
});
// Wait for any of multiple signals
const result = await ctx.waitForSignal(['approved', 'rejected'], {
timeout: '48h',
});
// Access signal payload
console.log('Approved by:', result.approvedBy);
// Wait for a single signal
Map<String, Object> approval = ctx.waitForSignal('order-approved', Map.of(
"timeout", "24h"
));
// Wait for any of multiple signals
Map<String, Object> result = ctx.waitForSignal(['approved', 'rejected'], Map.of(
"timeout", "48h"
));
// Access signal payload
System.out.println('Approved by:', result.approvedBy);
// Wait for a single signal
approval := ctx.waitForSignal('order-approved', {
"timeout": "24h",
});
// Wait for any of multiple signals
result := ctx.waitForSignal(['approved', 'rejected'], {
"timeout": "48h",
});
// Access signal payload
fmt.Println('Approved by:', result.approvedBy);
// Wait for a single signal
var approval = await ctx.waitForSignal('order-approved', {
["timeout"] = "24h",
});
// Wait for any of multiple signals
var result = await ctx.waitForSignal(['approved', 'rejected'], {
["timeout"] = "48h",
});
// Access signal payload
Console.WriteLine('Approved by:', result.approvedBy);
ctx.checkpoint() - Save Progress
- TypeScript
- Java
- Go
- .NET
// Save state for recovery
await ctx.checkpoint('payment-complete', {
chargeId: payment.id,
amount: ctx.input.amount,
timestamp: Date.now(),
});
// Save state for recovery
ctx.checkpoint('payment-complete', Map.of(
chargeId: payment.id,
amount: ctx.input.amount,
timestamp: Date.now()
));
// Save state for recovery
ctx.checkpoint('payment-complete', {
chargeId: payment.id,
amount: ctx.input.amount,
timestamp: Date.now(),
});
// Save state for recovery
await ctx.checkpoint('payment-complete', {
chargeId: payment.id,
amount: ctx.input.amount,
timestamp: Date.now(),
});
ctx.triggerRollback() - Manual Rollback
- TypeScript
- Java
- Go
- .NET
// Manually trigger rollback
if (fraudCheck.score > 0.8) {
const result = await ctx.triggerRollback('High fraud risk detected');
console.log('Rolled back steps:', result.rolled_back_steps);
return { success: false, reason: 'fraud_detected' };
}
// Manually trigger rollback
if (fraudCheck.score > 0.8) Map.of(
Map<String, Object> result = ctx.triggerRollback('High fraud risk detected');
System.out.println('Rolled back "steps", ", result.rolled_back_steps);
return Map.of( "success", false, reason: "fraud_detected' );
)
// Manually trigger rollback
if (fraudCheck.score > 0.8) {
result := ctx.triggerRollback('High fraud risk detected');
fmt.Println('Rolled back "steps": ", result.rolled_back_steps);
return { "success": false, reason: "fraud_detected' };
}
// Manually trigger rollback
if (fraudCheck.score > 0.8) {
var result = await ctx.triggerRollback('High fraud risk detected');
Console.WriteLine('Rolled back ["steps"] = ", result.rolled_back_steps);
return { ["success"] = false, reason: "fraud_detected' };
}
ctx.feature() - Child Features
- TypeScript
- Java
- Go
- .NET
// Run another feature as a child
const paymentResult = await ctx.feature(
'child-payment-123', // Child feature ID
'payment-processing', // Feature tag
{ amount: ctx.input.amount }, // Input
{
timeout: '5m',
retries: 2,
parent_close_policy: 'terminate',
}
);
// Run another feature as a child
Map<String, Object> paymentResult = ctx.feature(
'child-payment-123', // Child feature ID
'payment-processing', // Feature tag
Map.of( amount: ctx.input.amount ), // Input
Map.of(
"timeout", "5m",
"retries", 2,
"parent_close_policy", "terminate"
)
);
// Run another feature as a child
paymentResult := ctx.feature(
'child-payment-123', // Child feature ID
'payment-processing', // Feature tag
{ amount: ctx.input.amount }, // Input
{
"timeout": "5m",
"retries": 2,
"parent_close_policy": "terminate",
}
);
// Run another feature as a child
var paymentResult = await ctx.feature(
'child-payment-123', // Child feature ID
'payment-processing', // Feature tag
{ amount: ctx.input.amount }, // Input
{
["timeout"] = "5m",
["retries"] = 2,
["parent_close_policy"] = "terminate",
}
);
State Management
ctx.setState() and ctx.getState()
- TypeScript
- Java
- Go
- .NET
// Save state
ctx.setState('retryCount', 0);
ctx.setState('lastError', null);
// Later, retrieve state
const retryCount = ctx.getState<number>('retryCount') || 0;
if (retryCount < 3) {
ctx.setState('retryCount', retryCount + 1);
// Retry logic...
}
// Access all state
console.log('Current state:', ctx.state);
// Save state
ctx.setState('retryCount', 0);
ctx.setState('lastError', null);
// Later, retrieve state
Map<String, Object> retryCount = ctx.getState<number>('retryCount') || 0;
if (retryCount < 3) Map.of(
ctx.setState('retryCount', retryCount + 1);
// Retry logic...
)
// Access all state
System.out.println('Current state:', ctx.state);
// Save state
ctx.setState('retryCount', 0);
ctx.setState('lastError', null);
// Later, retrieve state
retryCount := ctx.getState<number>('retryCount') || 0;
if (retryCount < 3) {
ctx.setState('retryCount', retryCount + 1);
// Retry logic...
}
// Access all state
fmt.Println('Current state:', ctx.state);
// Save state
ctx.setState('retryCount', 0);
ctx.setState('lastError', null);
// Later, retrieve state
var retryCount = ctx.getState<number>('retryCount') || 0;
if (retryCount < 3) {
ctx.setState('retryCount', retryCount + 1);
// Retry logic...
}
// Access all state
Console.WriteLine('Current state:', ctx.state);
ctx.steps - Access Step Results
- TypeScript
- Java
- Go
- .NET
// Access results from previous steps
const userResult = ctx.steps['create-user'];
console.log('User ID:', userResult.id);
// Check completed steps
console.log('Completed:', ctx.completed_steps);
console.log('Current:', ctx.current_step);
// Access results from previous steps
Map<String, Object> userResult = ctx.steps['create-user'];
System.out.println('User "ID", ", userResult.id);
// Check completed steps
System.out.println(""Completed", ", ctx.completed_steps);
System.out.println("Current:', ctx.current_step);
// Access results from previous steps
userResult := ctx.steps['create-user'];
fmt.Println('User "ID": ", userResult.id);
// Check completed steps
fmt.Println(""Completed": ", ctx.completed_steps);
fmt.Println("Current:', ctx.current_step);
// Access results from previous steps
var userResult = ctx.steps['create-user'];
Console.WriteLine('User ["ID"] = ", userResult.id);
// Check completed steps
Console.WriteLine("["Completed"] = ", ctx.completed_steps);
Console.WriteLine("Current:', ctx.current_step);
Data Access
ctx.variable() and ctx.constant()
- TypeScript
- Java
- Go
- .NET
// Access app variables
const apiVersion = ctx.variable('stripe', 'api_version');
// Access app constants
const defaultCurrency = ctx.constant('stripe', 'default_currency');
// Access app variables
Map<String, Object> apiVersion = ctx.variable('stripe', 'api_version');
// Access app constants
Map<String, Object> defaultCurrency = ctx.constant('stripe', 'default_currency');
// Access app variables
apiVersion := ctx.variable('stripe', 'api_version');
// Access app constants
defaultCurrency := ctx.constant('stripe', 'default_currency');
// Access app variables
var apiVersion = ctx.variable('stripe', 'api_version');
// Access app constants
var defaultCurrency = ctx.constant('stripe', 'default_currency');
ctx.default() - Fallback Values
- TypeScript
- Java
- Go
- .NET
// Provide default values
const currency = ctx.default(ctx.input.currency, 'usd');
const retries = ctx.default(ctx.input.retries, 3);
// Provide default values
Map<String, Object> currency = ctx.default(ctx.input.currency, 'usd');
Map<String, Object> retries = ctx.default(ctx.input.retries, 3);
// Provide default values
currency := ctx.default(ctx.input.currency, 'usd');
retries := ctx.default(ctx.input.retries, 3);
// Provide default values
var currency = ctx.default(ctx.input.currency, 'usd');
var retries = ctx.default(ctx.input.retries, 3);
ctx.transform - Data Transformations
- TypeScript
- Java
- Go
- .NET
// String transformations
const upper = ctx.transform.upper(ctx.input.name);
const lower = ctx.transform.lower(ctx.input.email);
const trimmed = ctx.transform.trim(ctx.input.text);
// JSON operations
const parsed = ctx.transform.parseJson<MyType>(ctx.input.jsonString);
const stringified = ctx.transform.stringify(ctx.input.data);
// Date operations
const now = ctx.transform.now();
const formatted = ctx.transform.formatDate(Date.now(), 'YYYY-MM-DD');
// Array/object operations
const length = ctx.transform.length(ctx.input.items);
const size = ctx.transform.size(ctx.input.metadata);
const parts = ctx.transform.split(ctx.input.csv, ',');
const joined = ctx.transform.join(ctx.input.tags, ', ');
// String transformations
Map<String, Object> upper = ctx.transform.upper(ctx.input.name);
Map<String, Object> lower = ctx.transform.lower(ctx.input.email);
Map<String, Object> trimmed = ctx.transform.trim(ctx.input.text);
// JSON operations
Map<String, Object> parsed = ctx.transform.parseJson<MyType>(ctx.input.jsonString);
Map<String, Object> stringified = ctx.transform.stringify(ctx.input.data);
// Date operations
Map<String, Object> now = ctx.transform.now();
Map<String, Object> formatted = ctx.transform.formatDate(Date.now(), 'YYYY-MM-DD');
// Array/object operations
Map<String, Object> length = ctx.transform.length(ctx.input.items);
Map<String, Object> size = ctx.transform.size(ctx.input.metadata);
Map<String, Object> parts = ctx.transform.split(ctx.input.csv, ',');
Map<String, Object> joined = ctx.transform.join(ctx.input.tags, ', ');
// String transformations
upper := ctx.transform.upper(ctx.input.name);
lower := ctx.transform.lower(ctx.input.email);
trimmed := ctx.transform.trim(ctx.input.text);
// JSON operations
parsed := ctx.transform.parseJson<MyType>(ctx.input.jsonString);
stringified := ctx.transform.stringify(ctx.input.data);
// Date operations
now := ctx.transform.now();
formatted := ctx.transform.formatDate(Date.now(), 'YYYY-MM-DD');
// Array/object operations
length := ctx.transform.length(ctx.input.items);
size := ctx.transform.size(ctx.input.metadata);
parts := ctx.transform.split(ctx.input.csv, ',');
joined := ctx.transform.join(ctx.input.tags, ', ');
// String transformations
var upper = ctx.transform.upper(ctx.input.name);
var lower = ctx.transform.lower(ctx.input.email);
var trimmed = ctx.transform.trim(ctx.input.text);
// JSON operations
var parsed = ctx.transform.parseJson<MyType>(ctx.input.jsonString);
var stringified = ctx.transform.stringify(ctx.input.data);
// Date operations
var now = ctx.transform.now();
var formatted = ctx.transform.formatDate(Date.now(), 'YYYY-MM-DD');
// Array/object operations
var length = ctx.transform.length(ctx.input.items);
var size = ctx.transform.size(ctx.input.metadata);
var parts = ctx.transform.split(ctx.input.csv, ',');
var joined = ctx.transform.join(ctx.input.tags, ', ');
Logging
- TypeScript
- Java
- Go
- .NET
// Log messages at different levels
ctx.log.debug('Processing step', { step: 'create-user' });
ctx.log.info('User created', { userId: user.id });
ctx.log.warn('Retry attempt', { attempt: 2, maxRetries: 3 });
ctx.log.error('Step failed', { error: err.message });
// Log messages at different levels
ctx.log.debug('Processing step', Map.of( "step", "create-user" ));
ctx.log.info('User created', Map.of( userId: user.id ));
ctx.log.warn('Retry attempt', Map.of( "attempt", 2, "maxRetries", 3 ));
ctx.log.error('Step failed', Map.of( error: err.message ));
// Log messages at different levels
ctx.log.debug('Processing step', { "step": "create-user" });
ctx.log.info('User created', { userId: user.id });
ctx.log.warn('Retry attempt', { "attempt": 2, "maxRetries": 3 });
ctx.log.error('Step failed', { error: err.message });
// Log messages at different levels
ctx.log.debug('Processing step', { ["step"] = "create-user" });
ctx.log.info('User created', { userId: user.id });
ctx.log.warn('Retry attempt', { ["attempt"] = 2, ["maxRetries"] = 3 });
ctx.log.error('Step failed', { error: err.message });
Complete Example
This example uses branchOverrides so the compiler records the payment step even though the handler can early-return when validation fails. At runtime, the payment step runs only when $Step{validate}{available} is true.
- TypeScript
- Java
- Go
- .NET
await ductape.features.define({
tag: 'order-fulfillment',
name: 'Order Fulfillment',
description: 'Complete order processing feature',
options: {
timeout: 300000,
rollback_strategy: 'reverse_all',
},
signals: {
'cancel-order': { input: { reason: 'string' } },
},
// Record the "continue" branch so payment and later steps are in the schema
branchOverrides: { validate: { available: true } },
handler: async (ctx) => {
ctx.log.info('Starting order fulfillment', { orderId: ctx.input.orderId });
// Step 1: Validate order
const validation = await ctx.step('validate', async () => {
return ctx.api.run({
app: 'inventory-api',
event: 'check-availability',
input: { body: { items: ctx.input.items } },
});
});
if (!validation.available) {
return { success: false, reason: 'items_unavailable' };
}
// Step 2: Process payment (with rollback)
const payment = await ctx.step(
'payment',
async () => {
return ctx.api.run({
app: 'stripe',
event: 'create-charge',
input: {
body: {
amount: ctx.input.amount,
customer: ctx.input.customerId,
},
},
retries: 3,
});
},
async (result) => {
await ctx.api.run({
app: 'stripe',
event: 'refund',
input: { body: { chargeId: result.id } },
});
}
);
// Checkpoint after payment
await ctx.checkpoint('payment-complete', { chargeId: payment.id });
// Step 3: Reserve inventory (with rollback)
await ctx.step(
'reserve-inventory',
async () => {
return ctx.api.run({
app: 'inventory-api',
event: 'reserve',
input: { body: { items: ctx.input.items } },
});
},
async () => {
await ctx.api.run({
app: 'inventory-api',
event: 'release',
input: { body: { items: ctx.input.items } },
});
}
);
// Step 4: Create order record
const order = await ctx.step('create-order', async () => {
return ctx.database.insert({
database: 'orders-db',
event: 'create',
data: {
customer_id: ctx.input.customerId,
items: ctx.input.items,
total: ctx.input.amount,
payment_id: payment.id,
status: 'processing',
},
});
});
// Step 5: Send confirmation (non-critical)
await ctx.step(
'send-confirmation',
async () => {
await ctx.notification.email({
notification: 'transactional',
event: 'order-confirmed',
recipients: [ctx.input.email],
template: {
orderId: order.id,
items: ctx.input.items,
total: ctx.input.amount,
},
});
},
null,
{ allow_fail: true }
);
// Step 6: Queue for fulfillment
await ctx.step('queue-fulfillment', async () => {
await ctx.messaging.produce({
event: 'warehouse:new-order',
message: {
orderId: order.id,
items: ctx.input.items,
},
});
});
ctx.log.info('Order fulfilled successfully', { orderId: order.id });
return {
success: true,
orderId: order.id,
chargeId: payment.id,
};
},
});
ductape.features.define(Map.of(
"tag", "order-fulfillment",
"name", "Order Fulfillment",
"description", "Complete order processing feature",
options: Map.of(
"timeout", 300000,
"rollback_strategy", "reverse_all"
),
signals: Map.of(
'cancel-order': Map.of( input: Map.of( "reason", "string" ) )
),
// Record the "continue" branch so payment and later steps are in the schema
branchOverrides: Map.of( validate: Map.of( "available", true ) ),
handler: async (ctx) => Map.of(
ctx.log.info('Starting order fulfillment', Map.of( orderId: ctx.input.orderId ));
// Step 1: Validate order
Map<String, Object> validation = ctx.step('validate', async () => Map.of(
return ctx.api.run(Map.of(
"app", "inventory-api",
"event", "check-availability",
input: Map.of( body: Map.of( items: ctx.input.items ) )
));
));
if (!validation.available) Map.of(
return Map.of( "success", false, "reason", "items_unavailable" );
)
// Step 2: Process payment (with rollback)
Map<String, Object> payment = ctx.step(
'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,
customer: ctx.input.customerId
)
),
"retries", 3
));
),
async (result) => Map.of(
ctx.api.run(Map.of(
"app", "stripe",
"event", "refund",
input: Map.of( body: Map.of( chargeId: result.id ) )
));
)
);
// Checkpoint after payment
ctx.checkpoint('payment-complete', Map.of( chargeId: payment.id ));
// Step 3: Reserve inventory (with rollback)
ctx.step(
'reserve-inventory',
async () => Map.of(
return ctx.api.run(Map.of(
"app", "inventory-api",
"event", "reserve",
input: Map.of( body: Map.of( items: ctx.input.items ) )
));
),
async () => Map.of(
ctx.api.run(Map.of(
"app", "inventory-api",
"event", "release",
input: Map.of( body: Map.of( items: ctx.input.items ) )
));
)
);
// Step 4: Create order record
Map<String, Object> order = ctx.step('create-order', async () => Map.of(
return ctx.database.insert(Map.of(
"database", "orders-db",
"event", "create",
data: Map.of(
customer_id: ctx.input.customerId,
items: ctx.input.items,
total: ctx.input.amount,
payment_id: payment.id,
"status", "processing"
)
));
));
// Step 5: Send confirmation (non-critical)
ctx.step(
'send-confirmation',
async () => Map.of(
ctx.notification.email(Map.of(
"notification", "transactional",
"event", "order-confirmed",
recipients: [ctx.input.email],
template: Map.of(
orderId: order.id,
items: ctx.input.items,
total: ctx.input.amount
)
));
),
null,
Map.of( "allow_fail", true )
);
// Step 6: Queue for fulfillment
ctx.step('queue-fulfillment', async () => Map.of(
ctx.messaging.produce(Map.of(
"event", "warehouse:new-order",
message: Map.of(
orderId: order.id,
items: ctx.input.items
)
));
));
ctx.log.info('Order fulfilled successfully', Map.of( orderId: order.id ));
return Map.of(
"success", true,
orderId: order.id,
chargeId: payment.id
);
)
));
client.features.define({
"tag": "order-fulfillment",
"name": "Order Fulfillment",
"description": "Complete order processing feature",
options: {
"timeout": 300000,
"rollback_strategy": "reverse_all",
},
signals: {
'cancel-order': { input: { "reason": "string" } },
},
// Record the "continue" branch so payment and later steps are in the schema
branchOverrides: { validate: { "available": true } },
handler: async (ctx) => {
ctx.log.info('Starting order fulfillment', { orderId: ctx.input.orderId });
// Step 1: Validate order
validation := ctx.step('validate', async () => {
return ctx.api.run({
"app": "inventory-api",
"event": "check-availability",
input: { body: { items: ctx.input.items } },
});
});
if (!validation.available) {
return { "success": false, "reason": "items_unavailable" };
}
// Step 2: Process payment (with rollback)
payment := ctx.step(
'payment',
async () => {
return ctx.api.run({
"app": "stripe",
"event": "create-charge",
input: {
body: {
amount: ctx.input.amount,
customer: ctx.input.customerId,
},
},
"retries": 3,
});
},
async (result) => {
ctx.api.run({
"app": "stripe",
"event": "refund",
input: { body: { chargeId: result.id } },
});
}
);
// Checkpoint after payment
ctx.checkpoint('payment-complete', { chargeId: payment.id });
// Step 3: Reserve inventory (with rollback)
ctx.step(
'reserve-inventory',
async () => {
return ctx.api.run({
"app": "inventory-api",
"event": "reserve",
input: { body: { items: ctx.input.items } },
});
},
async () => {
ctx.api.run({
"app": "inventory-api",
"event": "release",
input: { body: { items: ctx.input.items } },
});
}
);
// Step 4: Create order record
order := ctx.step('create-order', async () => {
return ctx.database.insert({
"database": "orders-db",
"event": "create",
data: {
customer_id: ctx.input.customerId,
items: ctx.input.items,
total: ctx.input.amount,
payment_id: payment.id,
"status": "processing",
},
});
});
// Step 5: Send confirmation (non-critical)
ctx.step(
'send-confirmation',
async () => {
ctx.notification.email({
"notification": "transactional",
"event": "order-confirmed",
recipients: [ctx.input.email],
template: {
orderId: order.id,
items: ctx.input.items,
total: ctx.input.amount,
},
});
},
null,
{ "allow_fail": true }
);
// Step 6: Queue for fulfillment
ctx.step('queue-fulfillment', async () => {
ctx.messaging.produce({
"event": "warehouse:new-order",
message: {
orderId: order.id,
items: ctx.input.items,
},
});
});
ctx.log.info('Order fulfilled successfully', { orderId: order.id });
return {
"success": true,
orderId: order.id,
chargeId: payment.id,
};
},
});
await ductape.features.define({
["tag"] = "order-fulfillment",
["name"] = "Order Fulfillment",
["description"] = "Complete order processing feature",
options: {
["timeout"] = 300000,
["rollback_strategy"] = "reverse_all",
},
signals: {
'cancel-order': { input: { ["reason"] = "string" } },
},
// Record the "continue" branch so payment and later steps are in the schema
branchOverrides: { validate: { ["available"] = true } },
handler: async (ctx) => {
ctx.log.info('Starting order fulfillment', { orderId: ctx.input.orderId });
// Step 1: Validate order
var validation = await ctx.step('validate', async () => {
return ctx.api.run({
["app"] = "inventory-api",
["event"] = "check-availability",
input: { body: { items: ctx.input.items } },
});
});
if (!validation.available) {
return { ["success"] = false, ["reason"] = "items_unavailable" };
}
// Step 2: Process payment (with rollback)
var payment = await ctx.step(
'payment',
async () => {
return ctx.api.run({
["app"] = "stripe",
["event"] = "create-charge",
input: {
body: {
amount: ctx.input.amount,
customer: ctx.input.customerId,
},
},
["retries"] = 3,
});
},
async (result) => {
await ctx.api.run({
["app"] = "stripe",
["event"] = "refund",
input: { body: { chargeId: result.id } },
});
}
);
// Checkpoint after payment
await ctx.checkpoint('payment-complete', { chargeId: payment.id });
// Step 3: Reserve inventory (with rollback)
await ctx.step(
'reserve-inventory',
async () => {
return ctx.api.run({
["app"] = "inventory-api",
["event"] = "reserve",
input: { body: { items: ctx.input.items } },
});
},
async () => {
await ctx.api.run({
["app"] = "inventory-api",
["event"] = "release",
input: { body: { items: ctx.input.items } },
});
}
);
// Step 4: Create order record
var order = await ctx.step('create-order', async () => {
return ctx.database.insert({
["database"] = "orders-db",
["event"] = "create",
data: {
customer_id: ctx.input.customerId,
items: ctx.input.items,
total: ctx.input.amount,
payment_id: payment.id,
["status"] = "processing",
},
});
});
// Step 5: Send confirmation (non-critical)
await ctx.step(
'send-confirmation',
async () => {
await ctx.notification.email({
["notification"] = "transactional",
["event"] = "order-confirmed",
recipients: [ctx.input.email],
template: {
orderId: order.id,
items: ctx.input.items,
total: ctx.input.amount,
},
});
},
null,
{ ["allow_fail"] = true }
);
// Step 6: Queue for fulfillment
await ctx.step('queue-fulfillment', async () => {
await ctx.messaging.produce({
["event"] = "warehouse:new-order",
message: {
orderId: order.id,
items: ctx.input.items,
},
});
});
ctx.log.info('Order fulfilled successfully', { orderId: order.id });
return {
["success"] = true,
orderId: order.id,
chargeId: payment.id,
};
},
});
Next Steps
- Step Types - Detailed reference for all ctx methods
- Execution & Rollbacks - How execution works
- Examples - Real-world feature patterns