Docs/SDK/Library

Integration platform

Add your integration to the library

Writing run() is only the first step. To make an action appear in the workflow builder, wire it into the runtime registry (worker execution), the web display registry (icons and canvas labels), and the add-step menu (library list). This guide follows the exact paths in this repository.

Visual: code the definition → register → verify → deploy → users see it in the builder.

TL;DR

To make an integration visible and runnable you need four things:

  1. Executable definitionIntegrationDefinition with key, inputSchema, and run() (packages/integrations/src/registry.ts).
  2. Runtime registry entry — same file, object integrationRegistry maps "provider.action" → your definition. This is what the worker calls.
  3. Credential secret — users connect an account whose JSON matches authType (api_key, bearer_token, or oauth2). This powers the Connect Account flow and decrypts into run().
  4. Web UI wiringINTEGRATION_REGISTRY for canvas chrome + a row in add-step-chooser.tsx. This registry step is what makes the step appear in the library menu.

How it works (four moves)

Each card ties a concept to where it lives in code. Hover for a subtle focus state.

1

Define the integration

This file tells the platform how to execute the step: Zod inputs, auth type, and async run().

IntegrationDefinition
2

Register the action

This registry step is what wires the step key to your run() implementation for the worker.

integrationRegistry["acme.ping"]
3

Attach UI metadata

Display registry + chooser rows tell the builder which icon, label, and category to show.

INTEGRATION_REGISTRY + integrationItems
4

Load into the library

Without the web entries, the step runs in the worker but never appears in Add step → Integrations.

chooser + INTEGRATION_REGISTRY

Step 1 — Recommended project structure

This file tells the platform what the integration is at execution time: one exported IntegrationDefinition per action (e.g. slackSendMessage).

In this monorepo, definitions live together inside packages/integrations/src/registry.ts. For larger teams, split files logically — the important part is that they are imported into that module and added to integrationRegistry.

Recommended layout (logical)
integrations/
  slack/
    sendMessage.ts    # IntegrationDefinition + Zod schema
    sendEmail.ts      # optional second action
  acme/
    ping.ts
  registry.ts         # imports + integrationRegistry object
  • sendMessage.ts — action implementation (run, inputSchema, authType).
  • registry.ts — collects actions into integrationRegistry (worker entry point).
Why this matters
The worker only knows about keys present in integrationRegistry. File names are organizational; the exported object and its key field are authoritative.

Step 2 — The integration definition (your “manifest”)

There is no separate JSON manifest in this stack — the TypeScript object is the manifest. These fields drive runtime behavior and catalog metadata:

  • key — Globally unique step type id, usually provider.action (e.g. slack.sendMessage). Must match the builder + stored workflow JSON.
  • name — Human label in API catalog helpers (listIntegrationCatalog()).
  • provider — Namespace for credentials and for parsing the prefix before . in the canvas display helper.
  • authTypeapi_key | bearer_token | oauth2. This credential definition is what powers the Connect Account UI shape expected in the vault.
  • inputSchema — Zod schema for configJson inputs validated before run.
  • run — Async function receiving decrypted auth, parsed inputs, and ids (tenantId, runId, …). Return JSON becomes step output.
packages/integrations/src/registry.ts (excerpt pattern)
const slackSendMessage: IntegrationDefinition<unknown> = {
  key: "slack.sendMessage",
  name: "Send message",
  provider: "slack",
  authType: "oauth2",
  inputSchema: SlackSendMessageInputsSchema,
  run: async ({ auth, inputs }) => {
    const parsed = SlackSendMessageInputsSchema.parse(inputs);
    // call Slack Web API…
    return { ok: true };
  },
};

Step 3 — Register actions in the registry object

Import each definition and add it to integrationRegistry. This registration layer is what allows the worker to load executable steps via getIntegration(stepType).

packages/integrations/src/registry.ts
export const integrationRegistry: Record<string, IntegrationDefinition<unknown>> = {
  "slack.sendMessage": slackSendMessage,
  "gmail.sendEmail": gmailSendEmail,
  // "acme.ping": acmePing,
};

Step 4 — Credentials and authType

Credentials are stored per tenant (encrypted). When a run executes, the worker loads the bound credential and parses it with IntegrationAuthSchema. Your run() receives that object as auth.

This credential definition is what powers the Connect Account UI: the product expects users to save JSON matching the discriminated union (type: "oauth2" with access_token, etc.). Your integration code should branch on auth.type the same way existing Slack / Stripe helpers do in registry.ts.

Conceptual shape (stored secret)
// oauth2 example (simplified)
{
  "provider": "slack",
  "type": "oauth2",
  "access_token": "xoxb-…"
}

Step 5 — Global runtime registry (worker)

Critical

If your action is not in integrationRegistry, the worker cannot execute it — runs will fail when they hit that step type.

After editing packages/integrations, rebuild packages and redeploy worker-service (it imports @orchestrator/integrations). The web app can be updated separately for UI.

Step 6 — Making the library & canvas reflect your integration

Most missed step

If you skip either INTEGRATION_REGISTRY or integrationItems in add-step-chooser.tsx, users will not see a polished entry in the UI — even if the worker can run the step.

Canvas / node chromeapps/web/src/lib/integration-registry.ts maps the provider prefix (text before . in slack.sendMessage) to name, icon, and category. This file tells the builder how to draw the node (icon + title).

Add step → Integrations listapps/web/src/components/workflow-builder/add-step-chooser.tsx contains the integrationItems array. Each row's id must equal the integration key (e.g. slack.sendMessage). This registry step is what makes it appear in the library dropdown.

Future improvement: drive integrationItems from listIntegrationCatalog() via an API route so the menu stays in sync automatically — today it is explicit for each action.

Catalog entry (runtime)
{
  "key": "slack.sendMessage",
  "name": "Send message",
  "provider": "slack",
  "authType": "oauth2"
}
Builder picker row
S

Slack · Send message

Post to a channel with a connected workspace

Registry defines the executable step…

Animation: catalog fields ↔ how a picker row is presented (conceptual).

apps/web/src/lib/integration-registry.ts (pattern)
export const INTEGRATION_REGISTRY: Record<string, IntegrationRegistryEntry> = {
  slack: { name: "Slack", icon: "/icons/slack.svg", category: "Communication" },
  acme: { name: "Acme", icon: "/icons/acme.svg", category: "Core" },
};
apps/web/src/components/workflow-builder/add-step-chooser.tsx (pattern)
const integrationItems = [
  // …
  {
    id: "acme.ping",
    title: "Acme",
    action: "Ping",
    desc: "Health check your Acme workspace",
    badge: <BrandBadge letter="A" bgClass="bg-violet-600" />,
  },
] as const;

Step 7 — Test locally (checklist)

  1. npm run build (or workspace build) for @orchestrator/integrations and restart worker-service + api-service as you normally run them.
  2. Restart apps/web dev server after changing add-step-chooser or integration-registry.
  3. Open the workflow builder → Add stepIntegrations → search or scroll for your title.
  4. Drop the step onto the canvas — node should show the correct icon if INTEGRATION_REGISTRY is set for the prefix.
  5. Bind a real credential in the step inspector, then run a test workflow and confirm the worker log shows your run() path.

Step 8 — Publish / release

  • Validate Zod schemas and error handling for third-party APIs.
  • Test each authType path you claim to support.
  • Confirm icon file exists under apps/web/public/icons/ and paths match the registry.
  • Document the new key for your users (changelog / internal Notion).
  • Deploy registry updates: publish @orchestrator/integrations build artifact, roll worker + web together.

End-to-end mental model

One horizontal pass from code to runtime. Each node briefly highlights in sequence.

Action file

run() + inputSchema

integrationRegistry

Maps step key → definition

Credentials

Connect Account UI

INTEGRATION_REGISTRY

Canvas icon + title

add-step-chooser

Library list row

Builder

User adds step

Worker

Executes run()

Common mistakes

  • !Forgot add-step row — Worker runs acme.ping but users cannot pick it in the UI.
  • !Prefix mismatch — Key is acme.ping but INTEGRATION_REGISTRY only has slack; canvas falls back to generic labels.
  • Credential key mismatch — Saved secret type does not match authType your run() expects.
  • Wrong integration key string — Typo between workflow JSON and integrationRegistry map key.
  • Worker not redeployed — Web shows the step; execution still uses old bundle without your code.

Build your first visible integration (tabs)

Use the tabs to jump between the definition, auth notes, registry merge, and web wiring — fastest path to “I see it in the builder”.

// packages/integrations/src/registry.ts (pattern)
const AcmePingInputsSchema = z.object({ endpoint: z.string().url() });

const acmePing: IntegrationDefinition<unknown> = {
  key: "acme.ping",       // full step type id (must match builder + worker)
  name: "Ping",           // short label in catalogs
  provider: "acme",       // groups credentials + display prefix
  authType: "api_key",    // drives allowed credential JSON shape
  inputSchema: AcmePingInputsSchema,
  run: async ({ auth, inputs }) => {
    const parsed = AcmePingInputsSchema.parse(inputs);
    // use auth (decrypted), call API, return JSON → step output
    return { status: "ok" };
  },
};