Connect backends and workflows
How private workflows, API keys, backends and storage call each other, including publication, credentials, results and identity.
Step-by-step guidance.
Before you start
- A managed backend and a saved workflow in the same factory.
Connect in this order
- Save the workflow, then read `getWorkflowVersions`.
- Call `publishIntegrationWorkflow` with the `versionId` and the `deploymentRevision` as `expectedRevision`. It pins that version with private execution and removes broad grants. Keys cannot run drafts; `debugWorkflowRequest` tests drafts.
- Call `provisionApiKey` with a `name`, the `factoryId` and an explicit `expiresAt`, and without `groupIds`. It creates the factory’s storage if it does not exist yet.
- Bind the returned secret name to a backend environment variable, and configure the returned `workflowGatewayBaseUrl` and `storageApiBaseUrl`.
- Call the workflow through the starter’s client.
API keys and groups
| Item | Contract |
|---|---|
| `expiresAt` | Required. Within one year, or `null` for no expiry. |
| Storage | The key is stored in project secrets and returned by name, with `groupIds` and its metadata. |
| Default group | With no `groupIds`, it joins `<factoryId>_api_key_user_group`: `read`, `write` and `deepRead` on factory storage, plus execute and results (136) on every current and future workflow in the factory, including children. Workflows stay private and need no per-workflow grants. |
| Custom groups | Nonempty `groupIds` replace the default and get no automatic grants. `grantGroupAccess` uses 8 for execution or 136 for execution plus results and status, including called child workflows. |
| Revocation | `revokeApiKey` revokes access. Renewal is not automatic. |
Never make a workflow public to bypass permissions.
Call a workflow from a backend
The starter’s typed `src/workflows/client.ts` wraps `POST /wf/<workflowId>/trigger` with the `rumata-api-key` header and `{input: ...}`. The envelope is `{workflowRunId, status, result?, error?}`.
| Option | Effect |
|---|---|
| None | Inherits the saved response mode, synchronous when unset. |
| `{async:true}` | Background job. Returns a run ID to poll. |
| `{async:false}` | Overrides a saved async mode. |
- HTTP 200 alone does not mean completion. `queued`, `running` and `waiting` are pending even on HTTP 200.
- A terminal file-producing agent returns `result:{output,files}`, and its output schema describes `output`. Without files the result is direct.
- Use `workflowOutput(run.result)` from the starter, validate the payload and preserve files. Do not assume `result.pages` or other application fields exist.
Return a completed result
`status()` reads once. When an endpoint must return completed output, submit asynchronously once, save the run ID, then call `waitForSuccess(runId, {timeoutMs, signal})`.
const { workflowRunId } = await workflows.submit(workflowId, input, { async: true });
await saveRunId(job.id, workflowRunId);
const run = await workflows.waitForSuccess(workflowRunId, { timeoutMs: 60_000, signal });
return workflowOutput(run.result);- `waitForSuccess` returns only `succeeded`, or throws a safe failure, cancellation or deadline error. Map failure to a non-2xx response and keep the run ID.
- Never resubmit after an uncertain response.
- Keep 202 plus one-shot status reads for endpoints that intentionally expose background jobs.
Call a backend from a workflow
Use `getBackendService.endpoints.production`, a separate backend-scoped key, and an HTTP node header `rumata-api-key:{secretRef:'<name>'}`. The gateway strips credentials and caller-supplied identity, then supplies the authenticated `x-rumata-api-key-id`. Service-only routes can require the expected key ID. Trust this identity only behind the managed gateway.