Context API Reference
Complete reference for all methods available on the feature context (ctx) object.
ctx.step()
The callback must call a serializable ctx primitive, including ctx.functions. An arbitrary
application method such as ctx.step('register', () => authService.register(...)) cannot be
serialized and now fails compilation. See Portable functions.
Define a feature step with optional rollback.
- TypeScript
const result = await ctx.step<T>(
tag: string,
handler: () => Promise<T>,
rollback?: ((result: T) => Promise<void>) | null,
options?: IFeatureStepOptions
): Promise<T>;
Parameters
| Parameter | Type | Description |
|---|---|---|
tag | string | Unique step identifier |
handler | function | Async function that performs the step's work |
rollback | function | null | Optional function called during rollback |
options | object | Step configuration options |
Options
| Option | Type | Default | Description |
|---|---|---|---|
allow_fail | boolean | false | Continue feature if step fails |
retries | number | 0 | Number of retry attempts |
retry_delay | number | 1000 | Milliseconds between retries |
timeout | number | - | Step timeout in milliseconds |
critical | boolean | false | Always rollback this step on failure |
Examples
- TypeScript
// Basic step
const user = await ctx.step('create-user', async () => {
return ctx.database.insert({ database: 'users', event: 'create', data: {} });
});
// With rollback
const charge = await ctx.step(
'charge',
async () => ctx.api.run({ app: 'stripe', event: 'charge', input: { amount: 1000 } }),
async (result) => ctx.api.run({ app: 'stripe', event: 'refund', input: { chargeId: result.id } })
);
// With options
await ctx.step(
'notify',
async () => ctx.notification.email({ /* ... */ }),
null,
{ allow_fail: true, retries: 3 }
);
ctx.action
Call external APIs through connected apps.
ctx.api.run()
- TypeScript
const result = await ctx.api.run<T>({
app: string,
event: string,
input: Record<string, unknown>, // Flat input format
retries?: number,
timeout?: number,
}): Promise<T>;
Flat Input Format
Fields are automatically resolved to the correct location (body, params, query, or headers) based on the action's schema:
- TypeScript
input: {
amount: 1000, // auto-resolves to body.amount
currency: 'usd', // auto-resolves to body.currency
userId: '123' // auto-resolves to params.userId
}
For conflicting keys, use prefix syntax:
| Prefix | Target Location | Example |
|---|---|---|
body: | Request body | 'body:id': 'item_456' |
params: | Route parameters | 'params:id': 'user_123' |
query: | Query parameters | 'query:limit': 10 |
headers: | HTTP headers | 'headers:X-Custom': 'value' |
Example
- TypeScript
const charge = await ctx.api.run({
app: 'stripe',
event: 'create-charge',
input: {
amount: 1000,
currency: 'usd',
'headers:Idempotency-Key': ctx.feature_id
},
retries: 3,
timeout: 10000,
});
ctx.database
Database operations.
ctx.database.insert()
- TypeScript
const record = await ctx.database.insert<T>({
database: string,
event: string,
data: Record<string, unknown>,
}): Promise<T>;
ctx.database.query()
- TypeScript
const records = await ctx.database.query<T>({
database: string,
event: string,
params?: Record<string, unknown>,
}): Promise<T[]>;
ctx.database.update()
- TypeScript
const updated = await ctx.database.update<T>({
database: string,
event: string,
where: Record<string, unknown>,
data: Record<string, unknown>,
}): Promise<T>;
ctx.database.delete()
- TypeScript
const success = await ctx.database.delete({
database: string,
event: string,
where: Record<string, unknown>,
}): Promise<boolean>;
ctx.database.execute()
- TypeScript
const result = await ctx.database.execute<T>({
database: string,
event: string,
input: IDbActionRequest,
retries?: number,
timeout?: number,
}): Promise<T>;
ctx.graph
Graph database operations.
ctx.graph.createNode()
- TypeScript
const node = await ctx.graph.createNode<T>({
graph: string,
labels: string[],
properties: Record<string, unknown>,
}): Promise<T>;
ctx.graph.updateNode()
- TypeScript
const node = await ctx.graph.updateNode<T>({
graph: string,
id: string | number,
properties: Record<string, unknown>,
}): Promise<T>;
ctx.graph.deleteNode()
- TypeScript
await ctx.graph.deleteNode({
graph: string,
id: string | number,
}): Promise<void>;
ctx.graph.createRelationship()
- TypeScript
const rel = await ctx.graph.createRelationship<T>({
graph: string,
from: string,
to: string,
type: string,
properties?: Record<string, unknown>,
}): Promise<T>;
ctx.graph.deleteRelationship()
- TypeScript
await ctx.graph.deleteRelationship({
graph: string,
id: string | number,
}): Promise<void>;
ctx.graph.query()
- TypeScript
const results = await ctx.graph.query<T>({
graph: string,
action: string,
params?: Record<string, unknown>,
}): Promise<T[]>;
ctx.graph.execute()
- TypeScript
const result = await ctx.graph.execute<T>({
graph: string,
action: string,
input: Record<string, unknown>,
}): Promise<T>;
ctx.notification
Send notifications.
ctx.notification.email()
- TypeScript
await ctx.notification.email({
notification: string,
event: string,
recipients: string[],
subject: Record<string, unknown>,
template: Record<string, unknown>,
}): Promise<void>;
ctx.notification.sms()
- TypeScript
await ctx.notification.sms({
notification: string,
event: string,
phones: string[],
message: Record<string, unknown>,
}): Promise<void>;
ctx.notification.push()
- TypeScript
await ctx.notification.push({
notification: string,
event: string,
tokens: string[],
title: Record<string, unknown>,
body: Record<string, unknown>,
data?: Record<string, unknown>,
}): Promise<void>;
ctx.notification.send()
- TypeScript
await ctx.notification.send({
notification: string,
event: string,
input: INotificationRequest,
retries?: number,
}): Promise<void>;
ctx.storage
File operations.
ctx.storage.upload()
- TypeScript
const result = await ctx.storage.upload({
storage: string,
event: string,
input: {
buffer: Buffer | string,
fileName: string,
mimeType: string,
},
retries?: number,
}): Promise<{
file_key: string,
url?: string,
size?: number,
content_type?: string,
}>;
ctx.storage.download()
- TypeScript
const file = await ctx.storage.download({
storage: string,
event: string,
input: { file_key: string },
}): Promise<{
content: Buffer | string,
content_type?: string,
size?: number,
}>;
ctx.storage.delete()
- TypeScript
await ctx.storage.delete({
storage: string,
event: string,
input: { file_key: string },
}): Promise<void>;
ctx.messaging
Message broker (Ductape primitive). Use this to publish to broker topics.
ctx.messaging.produce()
- TypeScript
await ctx.messaging.produce({
event: string, // "broker-tag:topic-tag" (e.g. "order-events:payment-processed")
message: Record<string, unknown>,
}): Promise<void>;
ctx.publish
Deprecated. Prefer ctx.messaging.produce() above.
ctx.publish.send()
- TypeScript
await ctx.publish.send({
broker: string,
event: string,
input: {
message: Record<string, unknown>,
},
retries?: number,
}): Promise<void>;
ctx.quota
Rate limiting.
ctx.quota.execute()
- TypeScript
const result = await ctx.quota.execute<T>({
quota: string,
input: Record<string, unknown>,
timeout?: number,
}): Promise<T>;
ctx.fallback
Failure handling with alternatives.
ctx.fallback.execute()
- TypeScript
const result = await ctx.fallback.execute<T>({
fallback: string,
input: Record<string, unknown>,
timeout?: number,
}): Promise<T>;
ctx.healthcheck
Check service availability.
ctx.healthcheck.getStatus()
- TypeScript
const status = await ctx.healthcheck.getStatus(tag: string): Promise<{
status: 'available' | 'unavailable',
lastChecked?: string,
lastLatency?: number,
}>;
Control Flow
ctx.sleep()
Pause feature execution.
- TypeScript
await ctx.sleep(duration: number | string): Promise<void>;
| Format | Example | Description |
|---|---|---|
| number | 5000 | Milliseconds |
| string | '5s' | Seconds |
| string | '5m' | Minutes |
| string | '1h' | Hours |
| string | '1d' | Days |
ctx.waitForSignal()
Wait for an external signal.
- TypeScript
const payload = await ctx.waitForSignal<T>(
signal: string | string[],
options?: { timeout?: number | string }
): Promise<T>;
ctx.checkpoint()
Save state for recovery.
- TypeScript
await ctx.checkpoint(
name: string,
metadata?: Record<string, unknown>
): Promise<void>;
ctx.triggerRollback()
Manually trigger rollback.
- TypeScript
const result = await ctx.triggerRollback(reason: string): Promise<{
success: boolean,
rolled_back_steps: string[],
failed_steps?: Array<{ tag: string, error: string }>,
reason: string,
}>;
ctx.feature()
Run a child feature.
- TypeScript
const result = await ctx.feature<TInput, TOutput>(
childId: string,
tag: string,
input: TInput,
options?: {
timeout?: number | string,
retries?: number,
parent_close_policy?: 'terminate' | 'abandon' | 'request_cancel',
idempotency_key?: string,
}
): Promise<TOutput>;
State Management
ctx.setState()
- TypeScript
ctx.setState(key: string, value: unknown): void;
ctx.getState()
- TypeScript
const value = ctx.getState<T>(key: string): T | undefined;
Read-Only Properties
| Property | Type | Description |
|---|---|---|
ctx.state | object | All feature state |
ctx.steps | object | Results from completed steps |
ctx.completed_steps | string[] | Tags of completed steps |
ctx.current_step | string | null | Currently executing step |
Data Access
ctx.input
Read-only feature input data.
- TypeScript
const email = ctx.input.email;
const items = ctx.input.order.items;
ctx.variable()
Get app variable value.
- TypeScript
const value = ctx.variable(app: string, key: string): unknown;
ctx.constant()
Get app constant value.
- TypeScript
const value = ctx.constant(app: string, key: string): unknown;
ctx.token()
Get token value.
- TypeScript
const token = ctx.token(key: string): string;
ctx.default()
Provide fallback for undefined values.
- TypeScript
const value = ctx.default<T>(value: T | undefined, fallback: T): T;
ctx.transform
Data transformation utilities.
| Method | Description |
|---|---|
concat(...parts) | Concatenate literals and runtime operator values |
size(obj) | Number of keys in object |
length(arr) | Length of array or string |
parseJson<T>(str) | Parse JSON string |
stringify(obj) | Convert to JSON string |
upper(str) | Uppercase string |
lower(str) | Lowercase string |
trim(str) | Trim whitespace |
split(str, sep) | Split string |
join(arr, sep) | Join array |
now() | Current timestamp |
uuid() | New UUID generated for each Feature execution |
replace(str, search, replacement) | Replace all matching text at execution time |
substring(str, start, end) | Extract part of a string at execution time |
formatDate(date, fmt) | Format date |
- TypeScript
const reference = ctx.transform.concat(
'pay_',
ctx.transform.now(),
'_',
ctx.transform.uuid(),
);
This compiles to a portable expression such as $Concat(["pay_", $Now, "_", $Uuid], ""). Do not use Date.now() or Math.random() for Feature runtime values; those native calls run during compilation.
ctx.log
Logging methods.
- TypeScript
ctx.log.debug(message: string, data?: Record<string, unknown>): void;
ctx.log.info(message: string, data?: Record<string, unknown>): void;
ctx.log.warn(message: string, data?: Record<string, unknown>): void;
ctx.log.error(message: string, data?: Record<string, unknown>): void;
Metadata Properties
| Property | Type | Description |
|---|---|---|
ctx.feature_id | string | Unique execution ID |
ctx.feature_tag | string | Feature tag |
ctx.env | string | Environment (dev, staging, prd) |
ctx.product | string | Product tag |
ctx.context | object | Additional metadata |
ctx.session | object | Session info (if provided) |
ctx.auth | object | Authentication data |
Replay, Restart & Resume Properties
Properties for detecting and handling replayed, restarted, or resumed features:
| Property | Type | Description |
|---|---|---|
ctx.is_replay | boolean | Whether this is a replay execution |
ctx.is_restart | boolean | Whether this is a restart execution |
ctx.is_restored | boolean | Whether this is a resumed execution |
ctx.replayed_from | string | undefined | Original feature ID (if replay) |
ctx.restarted_from | string | undefined | Original feature ID (if restart) |
ctx.resumed_from | string | undefined | Original feature ID (if resume) |
ctx.replay_reason | string | undefined | Reason for replay |
ctx.restart_reason | string | undefined | Reason for restart |
ctx.restored_checkpoint | object | undefined | Checkpoint info (if resumed) |
ctx.original_input | object | undefined | Original input (for restart comparison) |
Example: Detecting Replay/Restart/Resume
- TypeScript
handler: async (ctx) => {
// Log execution context
if (ctx.is_replay) {
ctx.log.info('Replaying feature', {
original_id: ctx.replayed_from,
reason: ctx.replay_reason,
});
}
if (ctx.is_restart) {
ctx.log.info('Restarted feature', {
original_id: ctx.restarted_from,
reason: ctx.restart_reason,
original_input: ctx.original_input,
});
}
if (ctx.is_restored) {
ctx.log.info('Resuming feature', {
original_id: ctx.resumed_from,
checkpoint: ctx.restored_checkpoint?.name,
completed_steps: ctx.completed_steps,
});
// Skip already completed steps
if (ctx.completed_steps.includes('validate-order')) {
ctx.log.info('Skipping validate-order (already completed)');
}
}
// Feature logic...
}
SDK Feature Methods
Methods available on ductape.features for managing features outside of the handler context.
ductape.features.define()
Define and register a feature:
- TypeScript
const feature = await ductape.features.define({
product: string,
tag: string,
name: string,
description?: string,
options?: IFeatureOptions,
signals?: Record<string, { input: Record<string, string> }>,
queries?: Record<string, { handler: (ctx) => unknown }>,
envs?: Array<{ slug: string; active: boolean }>,
handler: (ctx: IFeatureContext) => Promise<TOutput>,
});
ductape.features.execute()
Execute a feature immediately:
- TypeScript
const result = await ductape.features.execute({
product: string,
env: string,
tag: string,
input: Record<string, unknown>,
feature_id?: string,
idempotency_key?: string,
timeout?: number,
context?: Record<string, unknown>,
});
ductape.features.dispatch()
Schedule a feature for later execution:
const result = await ductape.features.dispatch({
product: string,
env: string,
feature: string,
input: Record<string, unknown>,
schedule?: {
start_at?: number | string, // When to start
cron?: string, // Cron expression
every?: number, // Interval in ms
timezone?: string, // Timezone
limit?: number, // Max executions
endDate?: string, // End date
},
options?: {
retries?: number,
timeout?: number,
},
});
// Returns
interface IFeatureDispatchResult {
job_id: string;
status: 'scheduled' | 'queued';
scheduled_at: number;
recurring: boolean;
next_run_at?: number;
}
ductape.features.replay()
Re-execute a feature with the same input:
const result = await ductape.features.replay({
product: string,
env: string,
feature_id: string, // Original feature ID
options?: {
retries?: number,
timeout?: number,
debug?: boolean,
},
reason?: string, // Audit reason
idempotency_key?: string,
});
// Returns
interface IFeatureReplayResult extends IFeatureResult {
replayed_from: string; // Original feature ID
}
ductape.features.restart()
Re-execute a feature with new or modified input:
const result = await ductape.features.restart({
product: string,
env: string,
feature_id: string, // Original feature ID
input?: Record<string, unknown>, // New input (replaces original)
input_override?: Record<string, unknown>, // Partial override
merge_input?: boolean, // Merge with original input
reason?: string,
options?: IFeatureOptions,
});
// Returns
interface IFeatureRestartResult extends IFeatureResult {
restarted_from: string; // Original feature ID
}
ductape.features.resume()
Continue a paused or failed feature:
const result = await ductape.features.resume({
product: string,
env: string,
feature_id: string,
from_checkpoint?: string, // Resume from checkpoint
from_step?: string, // Resume from step
skip_steps?: string[], // Steps to skip
input?: Record<string, unknown>, // Additional input
});
// Returns
interface IFeatureResumeResult extends IFeatureResult {
resumed_from: string; // Original feature ID
resumed_checkpoint?: string;
}
ductape.features.replayFromStep()
Re-execute starting from a specific step:
- TypeScript
const result = await ductape.features.replayFromStep({
product: string,
env: string,
feature_id: string,
from_step: string, // Step to start from
step_outputs?: Record<string, unknown>, // Override previous step outputs
debug?: {
enabled: boolean,
pause_after_step?: boolean,
log_level?: 'info' | 'verbose' | 'debug',
},
});
ductape.features.signal()
Send a signal to a running feature:
- TypeScript
await ductape.features.signal({
product: string,
env: string,
feature_id: string,
signal: string, // Signal name
payload: Record<string, unknown>, // Signal data
});
ductape.features.query()
Query a running feature's state:
- TypeScript
const result = await ductape.features.query<T>({
product: string,
env: string,
feature_id: string,
query: string, // Query handler name
});
ductape.features.status()
Get feature execution status:
const status = await ductape.features.status({
product: string,
env: string,
feature_id: string,
});
// Returns
interface IFeatureStatus {
feature_id: string;
feature_tag: string;
status: 'running' | 'completed' | 'failed' | 'rolled_back' | 'cancelled';
current_step?: string;
completed_steps: string[];
state: Record<string, unknown>;
started_at: number;
updated_at: number;
}
ductape.features.cancel()
Cancel a running feature:
const result = await ductape.features.cancel({
product: string,
env: string,
feature_id: string,
reason?: string,
});
// Returns
interface IFeatureCancelResult {
cancelled: boolean;
rolled_back_steps: string[];
}
ductape.features.history()
Get feature execution history:
const history = await ductape.features.history({
product: string,
env: string,
feature_id: string,
include_step_details?: boolean,
include_rollback_details?: boolean,
});
// Returns
interface IFeatureHistory {
feature_id: string;
feature_tag: string;
status: FeatureStatus;
events: IFeatureEvent[];
checkpoints: ICheckpoint[];
replays?: string[];
restarts?: string[];
}
ductape.features.stepDetail()
Get detailed information about a step:
const detail = await ductape.features.stepDetail({
product: string,
env: string,
feature_id: string,
step_tag: string,
});
// Returns
interface IStepDetail {
tag: string;
name: string;
status: StepStatus;
input?: Record<string, unknown>;
output?: Record<string, unknown>;
error?: IStepError;
attempts: number;
start_time?: number;
end_time?: number;
duration?: number;
rollback_status?: RollbackStatus;
}
ductape.features.relatedExecutions()
List all related executions (replays, restarts, resumes):
const related = await ductape.features.relatedExecutions({
product: string,
env: string,
feature_id: string,
});
// Returns
interface IRelatedExecutions {
original: string;
executions: Array<{
feature_id: string;
type: 'original' | 'replay' | 'restart' | 'resume';
status: FeatureStatus;
created_at: number;
replayed_from?: string;
restarted_from?: string;
resumed_from?: string;
}>;
}
ductape.features.compare()
Compare two feature executions:
const comparison = await ductape.features.compare({
product: string,
env: string,
feature_ids: string[], // Two feature IDs to compare
});
// Returns
interface IExecutionComparison {
features: string[];
input_diff: Record<string, unknown[]>;
step_diffs: Array<{
step: string;
[feature_id: string]: {
status: StepStatus;
output?: Record<string, unknown>;
error?: string;
};
}>;
outcome_diff: Record<string, { status: FeatureStatus; output?: unknown }>;
}
Vector steps
Use ctx.vector inside ctx.step() to compile vector operations into the portable Feature definition. The executor supplies the Feature's product, environment, and inherited session at runtime.
- TypeScript
const matches = await ctx.step('find-related', () =>
ctx.vector.query({
vector: 'product-vectors',
values: ctx.input.embedding,
topK: 10,
filter: { tenantId: ctx.input.tenantId },
}),
);
await ctx.step('index-product', () =>
ctx.vector.upsertOne({
vector: 'product-vectors',
id: ctx.input.productId,
values: ctx.input.embedding,
metadata: { tenantId: ctx.input.tenantId },
}),
);
The complete context surface is:
ctx.vector.query({ vector, values, topK, namespace?, filter?, includeValues?, includeMetadata?, minScore? })ctx.vector.upsert({ vector, vectors, namespace?, wait? })ctx.vector.upsertOne({ vector, id, values, metadata?, namespace? })ctx.vector.deleteVectors({ vector, ids?, namespace?, deleteAll?, filter? })ctx.vector.execute({ vector, action, input })for a registered vector action
ctx.vector.query names the embedding field values; the compiled runtime payload uses vector. Vertex AI Vector Search does not generate embeddings, so supply a dimensionally compatible number[] from a registered model or application function.