Getting Started with Features
This guide walks you through creating and running your first feature using the code-first API.
Prerequisites
Before you begin, make sure you have:
- A Ductape account and workspace
- A product created in your workspace
- The Ductape SDK installed in your project
- At least one connected app or database
Step 1: Install the SDK
- TypeScript
- Java
- Go
- .NET
npm install @ductape/sdk@0.1.8
<dependency>
<groupId>app.ductape</groupId>
<artifactId>sdk</artifactId>
<version>0.1.8</version>
</dependency>
go get github.com/ductape/ductape/sdk/go@v0.1.8
dotnet add package Ductape.Sdk --version 0.1.8
Step 2: Initialize the SDK
- TypeScript
- Java
- Go
- .NET
import Ductape from '@ductape/sdk';
const ductape = new Ductape({
accessKey: 'your-access-key',
});
import app.ductape.sdk.Ductape;
import app.ductape.sdk.core.EnvType;
import app.ductape.sdk.core.RequestContext;
RequestContext auth = new RequestContext(null, null, null, null, 'your-access-key');
Ductape ductape = new Ductape(EnvType.PRODUCTION, auth);
import (
"context"
"github.com/ductape/ductape/sdk/go/core"
ductapesdk "github.com/ductape/ductape/sdk/go/ductape"
)
auth := core.NewRequestContext("", "", "", "", 'your-access-key')
client, err := ductapesdk.New(core.EnvProduction, auth)
if err != nil {
return err
}
using Ductape.Sdk;
using Ductape.Sdk.Core;
var auth = new RequestContext(null, null, null, null, 'your-access-key', null);
var ductape = new Ductape(EnvType.Production, auth);
See SDK runtime defaults — product and env belong on the constructor only.
Step 3: Define Your First Feature
Use ductape.features.define() to create a feature with a handler function:
- TypeScript
- Java
- Go
- .NET
const onboardingFeature = await ductape.features.define({
tag: 'user-onboarding',
name: 'User Onboarding',
description: 'Onboards new users with welcome email and audit log',
handler: async (ctx) => {
// Step 1: Create user in database
const user = await ctx.step('create-user', async () => {
return ctx.database.insert({
database: 'users-db',
event: 'create-user',
data: {
email: ctx.input.email,
name: ctx.input.name,
created_at: new Date().toISOString(),
},
});
});
// Step 2: Send welcome email
await ctx.step(
'send-welcome',
async () => {
await ctx.notification.email({
notification: 'transactional',
event: 'welcome-email',
recipients: [ctx.input.email],
subject: { name: ctx.input.name },
template: { name: ctx.input.name },
});
},
null, // No rollback needed for email
{ allow_fail: true } // Continue even if email fails
);
// Step 3: Create audit log
await ctx.step('audit-log', async () => {
return ctx.database.insert({
database: 'audit-db',
event: 'create-log',
data: {
action: 'user_created',
user_id: user.id,
timestamp: new Date().toISOString(),
},
});
});
// Return feature output
return {
success: true,
userId: user.id,
};
},
});
Map<String, Object> onboardingFeature = ductape.features.define(Map.of(
"tag", "user-onboarding",
"name", "User Onboarding",
"description", "Onboards new users with welcome email and audit log",
handler: async (ctx) => Map.of(
// Step 1: Create user in database
Map<String, Object> user = ctx.step('create-user', async () => Map.of(
return ctx.database.insert(Map.of(
"database", "users-db",
"event", "create-user",
data: Map.of(
email: ctx.input.email,
name: ctx.input.name,
created_at: Instant.now().toISOString()
)
));
));
// Step 2: Send welcome email
ctx.step(
'send-welcome',
async () => Map.of(
ctx.notification.email(Map.of(
"notification", "transactional",
"event", "welcome-email",
recipients: [ctx.input.email],
subject: Map.of( name: ctx.input.name ),
template: Map.of( name: ctx.input.name )
));
),
null, // No rollback needed for email
Map.of( "allow_fail", true ) // Continue even if email fails
);
// Step 3: Create audit log
ctx.step('audit-log', async () => Map.of(
return ctx.database.insert(Map.of(
"database", "audit-db",
"event", "create-log",
data: Map.of(
"action", "user_created",
user_id: user.id,
timestamp: Instant.now().toISOString()
)
));
));
// Return feature output
return Map.of(
"success", true,
userId: user.id
);
)
));
onboardingFeature := client.features.define({
"tag": "user-onboarding",
"name": "User Onboarding",
"description": "Onboards new users with welcome email and audit log",
handler: async (ctx) => {
// Step 1: Create user in database
user := ctx.step('create-user', async () => {
return ctx.database.insert({
"database": "users-db",
"event": "create-user",
data: {
email: ctx.input.email,
name: ctx.input.name,
created_at: new Date().toISOString(),
},
});
});
// Step 2: Send welcome email
ctx.step(
'send-welcome',
async () => {
ctx.notification.email({
"notification": "transactional",
"event": "welcome-email",
recipients: [ctx.input.email],
subject: { name: ctx.input.name },
template: { name: ctx.input.name },
});
},
null, // No rollback needed for email
{ "allow_fail": true } // Continue even if email fails
);
// Step 3: Create audit log
ctx.step('audit-log', async () => {
return ctx.database.insert({
"database": "audit-db",
"event": "create-log",
data: {
"action": "user_created",
user_id: user.id,
timestamp: new Date().toISOString(),
},
});
});
// Return feature output
return {
"success": true,
userId: user.id,
};
},
});
var onboardingFeature = await ductape.features.define({
["tag"] = "user-onboarding",
["name"] = "User Onboarding",
["description"] = "Onboards new users with welcome email and audit log",
handler: async (ctx) => {
// Step 1: Create user in database
var user = await ctx.step('create-user', async () => {
return ctx.database.insert({
["database"] = "users-db",
["event"] = "create-user",
data: {
email: ctx.input.email,
name: ctx.input.name,
created_at: DateTime.UtcNow.toISOString(),
},
});
});
// Step 2: Send welcome email
await ctx.step(
'send-welcome',
async () => {
await ctx.notification.email({
["notification"] = "transactional",
["event"] = "welcome-email",
recipients: [ctx.input.email],
subject: { name: ctx.input.name },
template: { name: ctx.input.name },
});
},
null, // No rollback needed for email
{ ["allow_fail"] = true } // Continue even if email fails
);
// Step 3: Create audit log
await ctx.step('audit-log', async () => {
return ctx.database.insert({
["database"] = "audit-db",
["event"] = "create-log",
data: {
["action"] = "user_created",
user_id: user.id,
timestamp: DateTime.UtcNow.toISOString(),
},
});
});
// Return feature output
return {
["success"] = true,
userId: user.id,
};
},
});
Step 4: Execute the Feature
- TypeScript
- Java
- Go
- .NET
const result = await ductape.features.execute({
tag: 'user-onboarding',
input: {
email: 'john@example.com',
name: 'John Doe',
},
});
if (result.status === 'completed') {
console.log('User onboarded successfully!');
console.log('User ID:', result.output.userId);
} else {
console.error('Onboarding failed:', result.error);
}
Map<String, Object> result = ductape.features.execute(Map.of(
"tag", "user-onboarding",
input: Map.of(
"email", "john@example.com",
"name", "John Doe"
)
));
if (result.status === 'completed') Map.of(
System.out.println('User onboarded successfully!');
System.out.println('User "ID", ", result.output.userId);
) else Map.of(
console.error("Onboarding failed:', result.error);
)
result := client.features.execute({
"tag": "user-onboarding",
input: {
"email": "john@example.com",
"name": "John Doe",
},
});
if (result.status === 'completed') {
fmt.Println('User onboarded successfully!');
fmt.Println('User "ID": ", result.output.userId);
} else {
console.error("Onboarding failed:', result.error);
}
var result = await ductape.features.execute({
["tag"] = "user-onboarding",
input: {
["email"] = "john@example.com",
["name"] = "John Doe",
},
});
if (result.status === 'completed') {
Console.WriteLine('User onboarded successfully!');
Console.WriteLine('User ["ID"] = ", result.output.userId);
} else {
console.error("Onboarding failed:', result.error);
}
Complete Example
Here's everything together:
- TypeScript
- Java
- Go
- .NET
import Ductape from '@ductape/sdk';
async function main() {
// Initialize SDK
const ductape = new Ductape({
user_id: 'your-user-id',
workspace_id: 'your-workspace-id',
private_key: 'your-private-key',
});
// Define the feature
await ductape.features.define({
tag: 'user-onboarding',
name: 'User Onboarding',
handler: async (ctx) => {
// Create user
const user = await ctx.step('create-user', async () => {
return ctx.database.insert({
database: 'users-db',
event: 'create-user',
data: {
email: ctx.input.email,
name: ctx.input.name,
},
});
});
// Send welcome email (non-critical)
await ctx.step(
'send-welcome',
async () => {
await ctx.notification.email({
notification: 'transactional',
event: 'welcome-email',
recipients: [ctx.input.email],
template: { name: ctx.input.name },
});
},
null,
{ allow_fail: true }
);
return { userId: user.id };
},
});
// Execute the feature
const result = await ductape.features.execute({
tag: 'user-onboarding',
input: {
email: 'jane@example.com',
name: 'Jane Doe',
},
});
console.log('Result:', result);
}
main().catch(console.error);
import app.ductape.sdk.Ductape;
import app.ductape.sdk.core.EnvType;
import app.ductape.sdk.core.RequestContext;
async function main() Map.of(
// Initialize SDK
Map<String, Object> ductape = new Ductape(Map.of(
"user_id", "your-user-id",
"workspace_id", "your-workspace-id",
"private_key", "your-private-key"
));
// Define the feature
ductape.features.define(Map.of(
"tag", "user-onboarding",
"name", "User Onboarding",
handler: async (ctx) => Map.of(
// Create user
Map<String, Object> user = ctx.step('create-user', async () => Map.of(
return ctx.database.insert(Map.of(
"database", "users-db",
"event", "create-user",
data: Map.of(
email: ctx.input.email,
name: ctx.input.name
)
));
));
// Send welcome email (non-critical)
ctx.step(
'send-welcome',
async () => Map.of(
ctx.notification.email(Map.of(
"notification", "transactional",
"event", "welcome-email",
recipients: [ctx.input.email],
template: Map.of( name: ctx.input.name )
));
),
null,
Map.of( "allow_fail", true )
);
return Map.of( userId: user.id );
)
));
// Execute the feature
Map<String, Object> result = ductape.features.execute(Map.of(
"tag", "user-onboarding",
input: Map.of(
"email", "jane@example.com",
"name", "Jane Doe"
)
));
System.out.println('Result:', result);
)
main();
import (
"context"
"github.com/ductape/ductape/sdk/go/core"
ductapesdk "github.com/ductape/ductape/sdk/go/ductape"
)
async function main() {
// Initialize SDK
ductape := new Ductape({
"user_id": "your-user-id",
"workspace_id": "your-workspace-id",
"private_key": "your-private-key",
});
// Define the feature
client.features.define({
"tag": "user-onboarding",
"name": "User Onboarding",
handler: async (ctx) => {
// Create user
user := ctx.step('create-user', async () => {
return ctx.database.insert({
"database": "users-db",
"event": "create-user",
data: {
email: ctx.input.email,
name: ctx.input.name,
},
});
});
// Send welcome email (non-critical)
ctx.step(
'send-welcome',
async () => {
ctx.notification.email({
"notification": "transactional",
"event": "welcome-email",
recipients: [ctx.input.email],
template: { name: ctx.input.name },
});
},
null,
{ "allow_fail": true }
);
return { userId: user.id };
},
});
// Execute the feature
result := client.features.execute({
"tag": "user-onboarding",
input: {
"email": "jane@example.com",
"name": "Jane Doe",
},
});
fmt.Println('Result:', result);
}
main().catch(console.error);
using Ductape.Sdk;
using Ductape.Sdk.Core;
async function main() {
// Initialize SDK
var ductape = new Ductape({
["user_id"] = "your-user-id",
["workspace_id"] = "your-workspace-id",
["private_key"] = "your-private-key",
});
// Define the feature
await ductape.features.define({
["tag"] = "user-onboarding",
["name"] = "User Onboarding",
handler: async (ctx) => {
// Create user
var user = await ctx.step('create-user', async () => {
return ctx.database.insert({
["database"] = "users-db",
["event"] = "create-user",
data: {
email: ctx.input.email,
name: ctx.input.name,
},
});
});
// Send welcome email (non-critical)
await ctx.step(
'send-welcome',
async () => {
await ctx.notification.email({
["notification"] = "transactional",
["event"] = "welcome-email",
recipients: [ctx.input.email],
template: { name: ctx.input.name },
});
},
null,
{ ["allow_fail"] = true }
);
return { userId: user.id };
},
});
// Execute the feature
var result = await ductape.features.execute({
["tag"] = "user-onboarding",
input: {
["email"] = "jane@example.com",
["name"] = "Jane Doe",
},
});
Console.WriteLine('Result:', result);
}
main().catch(console.error);
Understanding the Context
The ctx object passed to your handler provides:
- TypeScript
- Java
- Go
- .NET
handler: async (ctx) => {
// Input data
ctx.input.email // Access input fields
ctx.input.name
// Feature metadata
ctx.feature_id // Unique execution ID
ctx.feature_tag // 'user-onboarding'
ctx.env // 'dev', 'staging', 'prd'
ctx.product // 'my-product'
// Ductape components
ctx.action // API calls
ctx.database // Database operations
ctx.notification // Emails, SMS, push
ctx.storage // File uploads/downloads
ctx.graph // Graph database
ctx.publish // Message brokers
// Control flow
ctx.step() // Define a step
ctx.sleep() // Pause execution
ctx.checkpoint() // Save state
// State management
ctx.setState() // Save custom state
ctx.getState() // Retrieve state
// Logging
ctx.log.info() // Log messages
ctx.log.error()
}
handler: async (ctx) => Map.of(
// Input data
ctx.input.email // Access input fields
ctx.input.name
// Feature metadata
ctx.feature_id // Unique execution ID
ctx.feature_tag // 'user-onboarding'
ctx.env // 'dev', 'staging', 'prd'
ctx.product // 'my-product'
// Ductape components
ctx.action // API calls
ctx.database // Database operations
ctx.notification // Emails, SMS, push
ctx.storage // File uploads/downloads
ctx.graph // Graph database
ctx.publish // Message brokers
// Control flow
ctx.step() // Define a step
ctx.sleep() // Pause execution
ctx.checkpoint() // Save state
// State management
ctx.setState() // Save custom state
ctx.getState() // Retrieve state
// Logging
ctx.log.info() // Log messages
ctx.log.error()
)
handler: async (ctx) => {
// Input data
ctx.input.email // Access input fields
ctx.input.name
// Feature metadata
ctx.feature_id // Unique execution ID
ctx.feature_tag // 'user-onboarding'
ctx.env // 'dev', 'staging', 'prd'
ctx.product // 'my-product'
// Ductape components
ctx.action // API calls
ctx.database // Database operations
ctx.notification // Emails, SMS, push
ctx.storage // File uploads/downloads
ctx.graph // Graph database
ctx.publish // Message brokers
// Control flow
ctx.step() // Define a step
ctx.sleep() // Pause execution
ctx.checkpoint() // Save state
// State management
ctx.setState() // Save custom state
ctx.getState() // Retrieve state
// Logging
ctx.log.info() // Log messages
ctx.log.error()
}
handler: async (ctx) => {
// Input data
ctx.input.email // Access input fields
ctx.input.name
// Feature metadata
ctx.feature_id // Unique execution ID
ctx.feature_tag // 'user-onboarding'
ctx.env // 'dev', 'staging', 'prd'
ctx.product // 'my-product'
// Ductape components
ctx.action // API calls
ctx.database // Database operations
ctx.notification // Emails, SMS, push
ctx.storage // File uploads/downloads
ctx.graph // Graph database
ctx.publish // Message brokers
// Control flow
ctx.step() // Define a step
ctx.sleep() // Pause execution
ctx.checkpoint() // Save state
// State management
ctx.setState() // Save custom state
ctx.getState() // Retrieve state
// Logging
ctx.log.info() // Log messages
ctx.log.error()
}
Understanding the Result
The execution result contains:
- TypeScript
- Java
- Go
- .NET
{
status: 'completed', // 'completed', 'failed', or 'rolled_back'
feature_id: 'wf_abc123', // Unique execution ID
execution_time: 1250, // Total time in milliseconds
output: { // Your handler's return value
userId: 'usr_123'
},
completed_steps: [ // Steps that completed
'create-user',
'send-welcome',
'audit-log'
],
failed_step: null, // Step that failed (if any)
error: null // Error message if failed
}
Map.of(
"status", "completed", // 'completed', 'failed', or 'rolled_back'
"feature_id", "wf_abc123", // Unique execution ID
"execution_time", 1250, // Total time in milliseconds
output: Map.of( // Your handler's return value
"userId", "usr_123"
),
completed_steps: [ // Steps that completed
'create-user',
'send-welcome',
'audit-log'
],
failed_step: null, // Step that failed (if any)
error: null // Error message if failed
)
{
"status": "completed", // 'completed', 'failed', or 'rolled_back'
"feature_id": "wf_abc123", // Unique execution ID
"execution_time": 1250, // Total time in milliseconds
output: { // Your handler's return value
"userId": "usr_123"
},
completed_steps: [ // Steps that completed
'create-user',
'send-welcome',
'audit-log'
],
failed_step: null, // Step that failed (if any)
error: null // Error message if failed
}
{
["status"] = "completed", // 'completed', 'failed', or 'rolled_back'
["feature_id"] = "wf_abc123", // Unique execution ID
["execution_time"] = 1250, // Total time in milliseconds
output: { // Your handler's return value
["userId"] = "usr_123"
},
completed_steps: [ // Steps that completed
'create-user',
'send-welcome',
'audit-log'
],
failed_step: null, // Step that failed (if any)
error: null // Error message if failed
}
What Happens on Failure?
If a step fails:
- Steps with
allow_fail: true: Feature continues - Steps without
allow_fail: Feature stops and rolls back
For example, if send-welcome fails (and has allow_fail: true), the feature continues. If create-user fails, the feature fails immediately.
Adding Rollback Logic
To undo completed steps when something fails:
- TypeScript
- Java
- Go
- .NET
const payment = await ctx.step(
'charge-payment',
// Main handler
async () => {
return ctx.api.run({
app: 'stripe',
event: 'create-charge',
input: { body: { amount: ctx.input.amount } },
});
},
// Rollback handler - called if a later step fails
async (result) => {
await ctx.api.run({
app: 'stripe',
event: 'refund-charge',
input: { body: { chargeId: result.id } },
});
}
);
Map<String, Object> payment = ctx.step(
'charge-payment',
// Main 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 handler - called if a later step fails
async (result) => Map.of(
ctx.api.run(Map.of(
"app", "stripe",
"event", "refund-charge",
input: Map.of( body: Map.of( chargeId: result.id ) )
));
)
);
payment := ctx.step(
'charge-payment',
// Main handler
async () => {
return ctx.api.run({
"app": "stripe",
"event": "create-charge",
input: { body: { amount: ctx.input.amount } },
});
},
// Rollback handler - called if a later step fails
async (result) => {
ctx.api.run({
"app": "stripe",
"event": "refund-charge",
input: { body: { chargeId: result.id } },
});
}
);
var payment = await ctx.step(
'charge-payment',
// Main handler
async () => {
return ctx.api.run({
["app"] = "stripe",
["event"] = "create-charge",
input: { body: { amount: ctx.input.amount } },
});
},
// Rollback handler - called if a later step fails
async (result) => {
await ctx.api.run({
["app"] = "stripe",
["event"] = "refund-charge",
input: { body: { chargeId: result.id } },
});
}
);
When the feature rolls back, it calls each rollback handler with the original step's result.
Next Steps
Now that you've created your first feature:
- Step Types - Learn about all available ctx methods
- Execution & Rollbacks - Understand execution flow
- Examples - See real-world patterns
Troubleshooting
"Feature not found"
Make sure you've defined the feature with define() before executing.
"Step failed"
Check the result.error and result.failed_step for details.
"Input field not found"
Verify your input object contains all fields accessed via ctx.input.