Execute Component
Execute any Zafeguard component standalone via the SDK — CallBuilder pattern, inputs, config, delivery modes, and retry behavior.
Execute Component
Execute any component in the library standalone — no workflow required. Useful for one-off operations, testing integrations, or building your own orchestration.
Endpoint
POST /v1/sdk/components
Auth: X-Api-Key
Rate limit multiplier: 2×
TypeScript SDK — CallBuilder
The @zafeguard/caller-sdk exposes a CallBuilder pattern. workspace.call() validates inputs synchronously and returns a builder. Choose your delivery model by calling .execute() or .promise().
import { WorkspaceClient, ComponentModule } from '@zafeguard/caller-sdk';
const workspace = new WorkspaceClient({
apiKey: process.env.ZAFEGUARD_API_KEY!,
apiSecret: process.env.ZAFEGUARD_API_SECRET!,
});
const builder = workspace.call(ComponentModule.GET_EVM_ACCOUNT_BALANCE, {
jsonRpcUrl: 'https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY',
tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
account: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
});
// Delivery option A: wait for result (SSE stream)
const output = await builder.promise();
console.log(output.balance); // "1000000"
// Delivery option B: fire and get execution record
const execution = await builder.execute();
console.log(execution.id, execution.status);Validation errors are thrown synchronously from workspace.call() — before .execute() or .promise() is called and before any network request is made.
.execute(options?) — fire and return
Submits the component and returns the ExecuteComponentResponse immediately. The execution may still be CREATED or RUNNING when this resolves.
const execution = await workspace.call(ComponentModule.RANDOM_UUID, {}).execute();
// execution.completed may be false — use .id to poll or streamExecuteOptions
interface ExecuteOptions {
/**
* Number of execution attempts on failure (1–3).
* Retries are spaced apart automatically — call again with a higher
* `attempts` if transient upstream errors are expected.
* @default 1
*/
attempts?: number;
/**
* Wait up to this many milliseconds for the component to complete.
* If it finishes within the window, the response includes the output
* inline (`completed: true`). Max: 15000.
*/
waitForMs?: number;
/**
* HTTPS URL to receive the completion webhook.
* The payload is the full `ExecuteComponentResponse`.
*/
callbackUrl?: string;
/**
* HMAC-SHA256 signing key for the callback.
* The signature is sent in the `x-signature` header as `sha256=<hex>`.
* The signed payload is `{x-signature-timestamp}.{rawBody}`.
*/
callbackSecret?: string;
/**
* Custom HTTP headers added to the callback request.
*/
callbackHeaders?: Record<string, string>;
}Example — webhook delivery
await workspace.call(ComponentModule.WAIT_FOR_EVM_TRANSACTION, {
jsonRpcUrl: 'https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY',
transactionHash: '0x1234...',
}).execute({
callbackUrl: 'https://your-app.com/webhooks/wr',
callbackSecret: process.env.ZAFEGUARD_CALLBACK_SECRET,
callbackHeaders: { 'X-Source': 'zafeguard' },
attempts: 3,
});The callback payload is delivered with these headers:
| Header | Value |
|---|---|
x-delivery-id | {executionId}:{attemptNumber} |
x-event | component.execution.terminal |
x-signature-version | v1 (only when callbackSecret is set) |
x-signature-timestamp | ISO-8601 timestamp used in signature construction |
x-signature | sha256=<hex> (only when callbackSecret is set) |
| (custom) | Any headers from callbackHeaders |
See Webhooks for signature verification examples.
.promise(options?) — wait for result
Submits the component then opens an SSE stream and resolves with the typed component output once the execution completes. Closes the stream automatically on COMPLETED or FAILED.
const result = await workspace.call(ComponentModule.GET_EVM_ACCOUNT_BALANCE, {
jsonRpcUrl: 'https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY',
tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
account: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
}).promise();
console.log(result.balance); // "1000000".promise() uses SSE — not HTTP long-polling. The connection opens once, the server pushes a single event on completion, and the connection closes. See Streaming for the underlying protocol.
PromiseOptions
interface PromiseOptions {
/**
* Number of execution attempts on failure (1–3).
* @default 1
*/
attempts?: number;
/**
* Maximum time to wait for the result in milliseconds.
* If the execution doesn't complete within this window, the promise
* rejects with a `CallerSDKError` with message "Execution timed out".
* @default 60_000
*/
timeoutMs?: number;
}Example — with timeout
try {
const result = await workspace.call(ComponentModule.WAIT_FOR_EVM_TRANSACTION, {
jsonRpcUrl: 'https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY',
transactionHash: '0xabc...',
}).promise({ timeoutMs: 120_000, attempts: 2 });
console.log('Confirmed:', result);
} catch (err) {
if (err instanceof CallerSDKError && err.message === 'Execution timed out') {
console.warn('Transaction not confirmed within 2 minutes');
}
}Request body (REST)
interface ExecuteComponentRequest {
module: string; // Component module name (e.g. "RANDOM_UUID")
input: object; // Component inputs (varies per component)
config: object; // Component config (varies per component)
// Execution options
attempts?: number; // 1–3, default: 1
waitForMs?: number; // 0–15000 ms
callbackUrl?: string; // HTTPS webhook URL
callbackSecret?: string; // HMAC-SHA256 signing key
callbackHeaders?: Record<string, string>;
}module
The component module identifier. See Component Library for all available modules.
{ "module": "RANDOM_UUID" }
{ "module": "GET_EVM_ACCOUNT_BALANCE" }
{ "module": "SIGN_WITH_KEY_SHARE" }input and config
Each component defines its own input and config schemas. Find them in the Component Library. Fields marked secret are redacted from logs.
{
"module": "GET_EVM_ACCOUNT_BALANCE",
"input": {
"jsonRpcUrl": "https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY",
"tokenAddress": "0x0000000000000000000000000000000000000000",
"account": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
},
"config": {}
}Response
interface ExecuteComponentResponse {
id: string;
module: string;
status: 'CREATED' | 'RUNNING' | 'COMPLETED' | 'FAILED';
completed: boolean;
output?: unknown; // present when COMPLETED
error?: unknown; // present when FAILED
totalUsage: number; // credits consumed; 0 indicates a cache-served call
callback: {
url: string | null;
signed: boolean;
signatureAlgorithm: 'hmac-sha256-v1' | null;
headerNames: string[];
lastAttemptAt?: string | null;
deliveredAt?: string | null;
attemptCount: number;
lastError?: unknown | null;
};
createdAt: string;
updatedAt: string;
}Cache hits and credit usage
Some components return faster on repeat calls — typically those whose
output is fully determined by the input and config. For these
components, subsequent calls from your workspace with the same
inputs within the cache window are free (totalUsage = 0). Your
workspace's first call within the window always bills normally.
The contract is the same whether you invoke the component via the SDK direct-execute path or as a stage inside a workflow.
Billing summary
| Call | totalUsage |
|---|---|
| Your first call for these inputs within the window | The component's listed cost |
| Subsequent identical calls from your workspace within the window | 0 |
| After the window expires, the next call | The component's listed cost |
What counts as the same call
Two calls share a cache entry when:
- The component
moduleis identical. - The
inputandconfigobjects are deeply equal (the exact key/value shape, regardless of property order). - The component opts into caching (state-changing components and ones with side effects don't).
Different inputs never collide — a call for chainId: 1 doesn't share
a window with chainId: 137.
Response shape on a cache hit
| Aspect | First call in window | Subsequent calls in window |
|---|---|---|
| Response latency | Full call (depends on the component) | A few tens of milliseconds |
totalUsage | The component's listed cost | 0 |
status | Evolves CREATED → RUNNING → COMPLETED | Returns COMPLETED immediately |
id | Fresh execution id | Fresh execution id |
| Output value | Live result | Same as the most recent successful run |
Every call still creates an execution record, so the returned id is
fetchable via GET /v1/sdk/components/executions/:id.
Detecting a free call in your code
Check totalUsage === 0 on a COMPLETED response — that's the signal
this call was free:
const exec = await workspace
.call(ComponentModule.GET_EVM_USD_PRICE, { chainId: 1, tokenAddress: null })
.promise();
if (exec.totalUsage === 0) {
console.log('Free repeat call within the cache window');
}When caching does NOT apply
A fresh execution runs (billed normally) when:
- The component doesn't opt into caching (most state-changing components — signing, broadcasting to a custom chain, anything with side effects beyond an idempotent on-chain primitive).
- You supply a
callbackUrl— every call must fire its own webhook. - You set
attempts> 1 — retries always touch the wire. - The cache window has expired.
Pricing intent
The intent is to make read-heavy patterns — dashboards that re-fetch
the same token prices every few seconds, idempotent retries of the same
signed transaction, FOR_EACH loops that hit the same upstream — fast
and cheap without you needing to maintain a client-side cache. If you
need a fresh response for a specific call, structure your request so
its input differs (e.g., add a per-request salt) — that bypasses the
window and bills normally.
Examples
Instant component (REST)
Direct REST calls require X-Sdk-Timestamp and X-Sdk-Signature headers in addition to the API key. See Authentication → for the signing implementation. The TypeScript SDK handles this automatically.
import { createPrivateKey, sign } from 'crypto';
const signingKey = createPrivateKey({
key: Buffer.from(process.env.ZAFEGUARD_API_SECRET!, 'base64'),
format: 'der',
type: 'pkcs8',
});
const body = { module: 'RANDOM_UUID', input: {}, config: {}, waitForMs: 5000 };
const timestamp = Math.floor(Date.now() / 1000);
const message = `POST|/v1/sdk/components|${timestamp}|${JSON.stringify(body)}`;
const signature = sign(null, Buffer.from(message), signingKey).toString('base64');
const res = await fetch('https://api.zafeguard.com/v1/sdk/components', {
method: 'POST',
headers: {
'X-Api-Key': process.env.ZAFEGUARD_API_KEY!,
'X-Sdk-Timestamp': String(timestamp),
'X-Sdk-Signature': signature,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
const { output, totalUsage } = await res.json();
console.log(output.uuid); // "a4c2e8f1-..."SDK — promise pattern
import { WorkspaceClient, ComponentModule, CallerSDKError } from '@zafeguard/caller-sdk';
const workspace = new WorkspaceClient({
apiKey: process.env.ZAFEGUARD_API_KEY!,
apiSecret: process.env.ZAFEGUARD_API_SECRET!,
});
try {
// Inputs validated synchronously before request
const result = await workspace
.call(ComponentModule.GET_EVM_ACCOUNT_BALANCE, {
jsonRpcUrl: 'https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY',
tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
account: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
})
.promise();
console.log(result.balance); // fully typed
} catch (err) {
if (err instanceof CallerSDKError) {
console.error(err.message, err.details);
}
}SDK — execute pattern
import { WorkspaceClient, ComponentModule } from '@zafeguard/caller-sdk';
const workspace = new WorkspaceClient({
apiKey: process.env.ZAFEGUARD_API_KEY!,
apiSecret: process.env.ZAFEGUARD_API_SECRET!,
});
// Fire-and-forget: get execution ID, handle result via webhook
const execution = await workspace
.call(ComponentModule.WAIT_FOR_EVM_TRANSACTION, {
jsonRpcUrl: 'https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY',
transactionHash: '0x1234...',
})
.execute({
callbackUrl: 'https://your-app.com/webhooks/wr',
});
console.log(`Tracking execution ${execution.id}`); // status: "CREATED"Get execution status
GET /v1/sdk/components/executions/:executionId
Fetch the current state of any execution by ID. Use this when you've submitted a job with .execute() and want to check it later.
import { WorkspaceClient, ComponentModule } from '@zafeguard/caller-sdk';
const workspace = new WorkspaceClient({
apiKey: process.env.ZAFEGUARD_API_KEY!,
apiSecret: process.env.ZAFEGUARD_API_SECRET!,
});
// 1. Submit without waiting
const execution = await workspace
.call(ComponentModule.WAIT_FOR_EVM_TRANSACTION, {
jsonRpcUrl: 'https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY',
transactionHash: '0x1234...',
})
.execute();
console.log('Tracking:', execution.id); // status: "CREATED"
// 2. Check status later
const latest = await workspace.execution.get(execution.id);
if (latest.completed) {
console.log('Output:', latest.output);
} else if (latest.status === 'FAILED') {
console.error('Error:', latest.error);
} else {
console.log('Still running:', latest.status);
}For real-time updates without polling, use workspace.execution.stream() or .promise(). See below.
Stream execution status
GET /v1/sdk/components/executions/:executionId/stream
Subscribe to live SSE updates for any execution by ID. The stream closes automatically when the execution reaches COMPLETED or FAILED.
import { WorkspaceClient, ComponentModule } from '@zafeguard/caller-sdk';
const workspace = new WorkspaceClient({
apiKey: process.env.ZAFEGUARD_API_KEY!,
apiSecret: process.env.ZAFEGUARD_API_SECRET!,
});
// 1. Submit fire-and-forget
const execution = await workspace
.call(ComponentModule.WAIT_FOR_EVM_TRANSACTION, {
jsonRpcUrl: 'https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY',
transactionHash: '0x1234...',
})
.execute();
// 2. Stream status updates
const sub = workspace.execution.stream(execution.id, {
onUpdate(event) {
console.log(event.status, event.output);
// stream closes automatically on COMPLETED / FAILED
},
onError(err) {
console.error('Stream error:', err.message);
},
});
// Cancel early if needed
// sub.close();workspace.execution.stream() is the lower-level primitive. Use .promise() on workspace.call() if you just want to await the typed output — it uses the same SSE stream internally.
Redeliver a failed callback
POST /v1/sdk/components/executions/:executionId/replay-callback
Re-delivers the completion webhook if the original delivery failed. Idempotent — safe to call multiple times.
import { WorkspaceClient, ComponentModule } from '@zafeguard/caller-sdk';
const workspace = new WorkspaceClient({
apiKey: process.env.ZAFEGUARD_API_KEY!,
apiSecret: process.env.ZAFEGUARD_API_SECRET!,
});
// 1. Submit with a webhook
const execution = await workspace
.call(ComponentModule.BROADCAST_EVM_TRANSACTION, {
jsonRpcUrl: 'https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY',
signedTransaction: '0x...',
})
.execute({
callbackUrl: 'https://your-app.com/webhooks/wr',
callbackSecret: process.env.ZAFEGUARD_CALLBACK_SECRET,
});
// 2. If the webhook wasn't delivered, retry it
await workspace.webhook.redeliver(execution.id);workspace.webhook.redeliver() only works when callbackUrl was set at execution time.