> ## Documentation Index
> Fetch the complete documentation index at: https://docs.useorbitcrm.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Apps in Workflows

> Let the workspaces that install your app use it as a step in their workflows: declare actions, receive signed deliveries, return outputs.

Orbit workflows run on form submissions, booked meetings, deal stage changes and imports, and send data to CRMs, Slack, webhooks and more. Your app can be one of those steps. Turn on **Available in workflows** for the app in the [Developer Portal](https://docs.useorbitcrm.ai/apps/portal.md), declare the actions it offers, and every workspace that has installed your app sees them in the workflow editor's **Installed apps** section as "*App*: *Action*".

## Do you need this?

Two kinds of apps exist, and the difference is only where the app shows up:

| | API and webhooks | API, webhooks and workflows |
| - | - | - |
| Connects over OAuth, reads and writes the workspace's data | Yes | Yes |
| Receives webhook events | Yes | Yes |
| Appears in the workflow editor as a step people can add | No | Yes, as "*App*: *Action*" under **Installed apps** |
| What you build | Your own sync, on your schedule | An HTTPS endpoint per action that receives signed deliveries when a workflow runs |
| Where to choose | The *Where your app appears* choice when you register the app, or the **Workflows** card on the app page later | |

Pick **API and webhooks** when your app pulls or pushes data on its own (a CRM sync, an analytics import, an enrichment tool that reads contacts). Pick **workflows** when a workspace should be able to decide, per workflow, to send something to your app at a particular step ("when this form is submitted, send the event to *App*"). The portal shows a preview of the step before you turn it on, and you can change your mind later: turning it on for an approved app is a [change request](https://docs.useorbitcrm.ai/apps/app-review.md#changing-an-approved-app) (you get the signing secret and can define actions right away; workspaces see the app once the request is approved), turning it off applies immediately.

Running the step POSTs the inputs the workspace mapped to your endpoint, signed with your app's action signing secret. Nothing is sent to workspaces that have not installed the app, and deliveries stop on their own when a workspace uninstalls it.

## Declare an action

An action has:

| Field | Meaning |
| - | - |
| `key` | Stable snake\_case identifier your endpoint switches on, unique per app (`send_event`) |
| `name` | What the workspace sees (`Send event`) |
| `description` | Optional, shown under the step in the editor |
| `endpoint_url` | HTTPS URL on a public host that receives deliveries |
| `inputs` | The values a workspace maps when it adds the step: each has a `key`, a `label`, a `type` (`string`, `number`, `boolean`, `select`), whether it is `required`, and an optional `description`. A `select` also has `options` or an `options_url` ([Dropdowns](#dropdowns)) |

Declare up to 20 actions per app and 30 inputs per action. The workspace maps each input from its workflow fields (`{{email}}`, `{{deal.amount}}`, a literal) in the step's panel; required inputs must be mapped before the workflow can be activated, and a run whose required input renders empty for that contact fails the step without calling you.

An input's `description` is the help text under its box in the step's panel. Use it to say what to enter and where to find it ("The event name your Halo trigger listens for"); without one, the panel shows a generic hint for the type.

## Dropdowns

When the value is one of a set, declare the input with `"type": "select"` and the workspace picks it from a searchable list instead of typing it. The choices come from one of two places:

* **A fixed list**, `options`: up to 100 choices, each `{ "value": "high", "label": "High priority" }` or just `"high"`. Use it for values that are the same in every workspace.
* **Listed by your app**, `options_url`: an HTTPS URL on a public host. Use it for values that live in the workspace's own account in your app: its workflows, lists, custom fields.

```json theme={null}
"inputs": [
  { "key": "workflow_id", "label": "Workflow", "type": "select", "required": true, "options_url": "https://api.yourapp.com/orbit/options", "description": "The workflow to start for this contact" },
  { "key": "priority", "label": "Priority", "type": "select", "options": [{ "value": "high", "label": "High" }, { "value": "low", "label": "Low" }] }
]
```

Whenever someone opens the step's panel in the workflow editor or opens the list, Orbit POSTs to `options_url`, signed with your action signing secret exactly like a delivery. One request at a time: an opening while a request is in flight, or within a couple of seconds of the last one, reuses that request instead of sending another. Answer with the choices as they are now (an event created in your app a moment ago included); Orbit shows the previous answer until yours arrives and never keeps one beyond that:

```json theme={null}
{
  "id": "6f1c0d2e-...",
  "event": "workflow.action_options",
  "app": { "client_id": "orb_client_..." },
  "action": { "key": "enroll_in_workflow" },
  "input": { "key": "workflow_id" },
  "team_id": "7c1d0e2f-...",
  "occurred_at": "2026-10-30T12:00:00.000Z"
}
```

The headers are a delivery's, with `X-Orbit-Event: workflow.action_options` and `X-Orbit-Input` naming the input. One URL can serve every input: switch on `input.key`. Answer within 8 seconds with that workspace's choices:

```json theme={null}
{ "options": [{ "value": "wf_123", "label": "Onboarding nurture", "description": "5 emails over 2 weeks" }] }
```

* `value` (text or a number, at most 200 characters) is what the delivery's `inputs` carries. `label` is what the list shows and defaults to the value; `description` is optional. The panel shows up to 500 choices and searches them.
* When you cannot list choices for this workspace (no account in your app is linked to this `team_id`, say), answer a `4xx` with `{ "error": "Connect Orbit in YourApp to pick a workflow" }`. The panel shows your message under the dropdown. Any other failure shows "*App*: the app answered *status*".
* The panel also lets the workspace switch a dropdown to a `{{field}}` template ("Use a field") for a value that differs per contact, and choices are not re-checked when the step runs. Treat a select's value like any input: if the choice no longer exists in your app, answer the delivery with a `4xx` that says so.

A delivery carries a select's value as text. Workflow validation refuses a fixed-list input set to anything other than one of its values or a template.

Manage actions on the app's page in the portal, or over the API with the app owner's session:

| Method | Endpoint | Description |
| - | - | - |
| `PATCH` | `/api/developer/apps/:id` | `{ "workflow_actions_enabled": true }` turns the feature on; the first time, the response carries `action_signing_secret` once |
| `GET` / `POST` | `/api/developer/apps/:id/actions` | List / create (`key`, `name`, `description`, `endpoint_url`, `inputs`, `is_active`) |
| `PATCH` / `DELETE` | `/api/developer/apps/:id/actions/:actionId` | Change any field or turn the action off (`is_active: false`) / delete it |
| `POST` | `/api/developer/apps/:id/actions/secret` | Rotate the signing secret (returned once) |

Turning an action off keeps the steps workspaces built; they fail with "the app turned off *Action*" until it is on again. Deleting it does the same, for good.

## The delivery

Each run of the step is one `POST` to your endpoint:

```json theme={null}
{
  "id": "0b3f6c8e-...",
  "event": "workflow.action",
  "app": { "client_id": "orb_client_..." },
  "action": { "key": "send_event", "name": "Send event" },
  "team_id": "7c1d0e2f-...",
  "workflow": { "id": "e4a1...", "name": "Demo requests", "run_id": "9f2b...", "node_id": "3c7d..." },
  "trigger": { "submission_id": "5a1e...", "form_id": "88c0..." },
  "contact": { "id": "5c6d7e8f-...", "email": "ada@acme.com" },
  "inputs": { "event_name": "purchase", "amount": 1200, "is_trial": false },
  "occurred_at": "2026-10-27T20:00:00.000Z"
}
```

`team_id` is the workspace that installed your app (the same id `GET /api/v1/me` and the webhook envelope carry), so you can look up the right account. `inputs` holds every declared input, typed as declared; an optional input the workspace left unmapped is `null`. Use `contact.id` with `GET /api/v1/contacts/:id` for anything beyond what was mapped.

Headers:

| Header | Value |
| - | - |
| `X-Orbit-Signature` | `sha256=<HMAC-SHA256 of the raw body with your action signing secret>` |
| `X-Orbit-Event` | `workflow.action` |
| `X-Orbit-Action` | The action `key` |
| `X-Orbit-App` | Your app's `client_id` |
| `X-Orbit-Delivery-ID` | The body's `id`, unique per attempt |
| `X-Orbit-Timestamp` | The body's `occurred_at` |
| `X-Orbit-Attempt` | Present on retries |

Verify the signature exactly as for [webhooks](https://docs.useorbitcrm.ai/developers/webhooks/security.md), with the action signing secret instead of the webhook secret. The secret is shown once when you turn the feature on and can be rotated from the portal; rotate it before you change your verification, as deliveries use the new secret from the next run.

## Your response

* A `2xx` means the step succeeded. A JSON body with an `outputs` object (`{ "outputs": { "cometly_event_id": "evt_9" } }`) is kept with the run and available to later steps; values are strings, numbers or booleans, at most 50 keys.
* `408`, `425`, `429`, any `5xx`, a timeout (20 seconds) or a connection error is retried up to three times with backoff, like any destination step.
* Any other `4xx` is treated as your app refusing the call: the step fails at once with your response text in the run's error, and is not retried.

## For workspaces and agents

In the editor, installed apps' actions appear under **Installed apps** in the add-step menu once the app is installed; each step's panel shows your inputs with the workspace's field picker, and your dropdowns for `select` inputs. Over the API and MCP, `GET /api/v1/workflows/schema` lists them under `app_actions`, and a workflow node with `"type": "destination"`, `"provider": "app_action"` and `config` `{ app_id, action_id, action_key, inputs: { "<input key>": "<template>" } }` uses one. The schema shows a fixed list's `options` and marks an input your app lists with `"dynamic_options": true`; the `options_url` itself, like the endpoint, is never shown to workspaces.

> [card] [Webhooks](https://docs.useorbitcrm.ai/apps/webhooks.md)
>
> Tell your app what happened in a workspace

> [card] [Workflows API](https://docs.useorbitcrm.ai/developers/api/workflows.md)
>
> Build and run workflows from code


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.