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:


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 stream

ExecuteOptions

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:

HeaderValue
x-delivery-id{executionId}:{attemptNumber}
x-eventcomponent.execution.terminal
x-signature-versionv1 (only when callbackSecret is set)
x-signature-timestampISO-8601 timestamp used in signature construction
x-signaturesha256=<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

CalltotalUsage
Your first call for these inputs within the windowThe component's listed cost
Subsequent identical calls from your workspace within the window0
After the window expires, the next callThe component's listed cost

What counts as the same call

Two calls share a cache entry when:

  • The component module is identical.
  • The input and config objects 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

AspectFirst call in windowSubsequent calls in window
Response latencyFull call (depends on the component)A few tens of milliseconds
totalUsageThe component's listed cost0
statusEvolves CREATED → RUNNING → COMPLETEDReturns COMPLETED immediately
idFresh execution idFresh execution id
Output valueLive resultSame 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.

On this page