# Set up an event-ingest workflow and check batch status

Event ingest is how a system hands Returning.AI a batch of rows and lets a workflow run once for each accepted batch. The API builds and checks the workflow; the batch's own lifecycle is read through the public [event ingest status endpoint](https://docs.returning.ai/api-reference/data-workflows/read-event-ingest-lifecycle-status-by-trigger-and-batch-id.md).

This page combines the two questions that always arrive together: "is my workflow configured for my envelope?" and "did my batch go through?".

## The envelope contract

A sender posts a JSON envelope with three things: the trigger id at the root, the batch id at the root path the trigger is configured for, and an array of rows, each carrying the record id fields the trigger names.

`Event payload`

```json
{
  "trigger_id": "<PRODUCTION_TRIGGER_ID>",
  "metadata": { "batch_id": "net-deposit-20260710-001" },
  "data": [
    { "occurred_at": "2026-07-10T12:21:24.001Z", "event_type": "net_deposit.snapshot", "client_id": "<broker-client-id>", "deposit_id": "18774", "net_deposit_delta": 10000 }
  ]
}
```

That example shows only the event shape a batch carries. It is not a Workflow Builder API request; the batch goes to the event ingest endpoint your account was given, not to the base URL on these pages. Packaging and sending trade batches is covered in [Trading volume and rewards](https://docs.returning.ai/broker-integrations/trading-rewards.md) and [SQS data feed](https://docs.returning.ai/broker-integrations/sqs.md).

## 1. Create the workflow with the ingest trigger

`Create a workflow`

```bash
curl --request POST \
  --url "https://api.returning.ai/v1/workflow-builder/data-workflows" \
  --header "Authorization: Bearer <API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{
    "name": "Net deposits",
    "trigger": {
      "type": "event-ingest",
      "name": "Deposits",
      "options": {
        "batchIdPath": "metadata.batch_id",
        "recordIdPaths": ["deposit_id"]
      }
    }
  }'
```

Readback: the trigger's `_id` and its stored `options`. The `_id` is the value the sender posts as `trigger_id`.

To change the paths later, replace the trigger:

`Create or replace the trigger`

```bash
curl --request PUT \
  --url "https://api.returning.ai/v1/workflow-builder/data-workflows/65f1a2b3c4d5e6f708192a3b/trigger" \
  --header "Authorization: Bearer <API_KEY>" \
  --header "Content-Type: application/json" \
  --header "If-Match: 162af81f5de4ff2b557a4087de9a9a73a0ed4e621f737787e35a93614cb50d1c" \
  --data '{
    "type": "event-ingest",
    "name": "Deposits",
    "options": {
      "batchIdPath": "metadata.batch_id",
      "recordIdPaths": ["deposit_id", "transfer_id"]
    }
  }'
```

## 2. Understand the two deduplication layers

- **The batch id** identifies one submission. The same batch id again is skipped before any row is checked, so an exact retry is always safe and always pays nothing.
- **A record id** identifies one row. A record id seen before is skipped even inside a different batch, and other new rows in that batch still run.

Multiple record id paths are independent checks, not a combined key: a repeat of any one of them makes the row a duplicate, and the path name is not part of the identity. If one business record needs several fields, build one namespaced value before sending it, for example `server-2:account-88:trade-100`, and configure that single path.

A row with no value at any configured record id path is counted as missing and skipped.

## 3. Route the rows to the right step, then validate

Take every field key from the catalog rather than guessing one: [`GET /catalog`](https://docs.returning.ai/api-reference/workflow-builder/list-step-types-and-trigger-types.md) lists them in `platformActionFields.get` (read) and `platformActionFields.post` (write). See [Field keys come from the catalog](https://docs.returning.ai/workflow-builder.md#field-keys-come-from-the-catalog).

The trigger's rows are in `data`. Map the array, not the envelope, unless a step needs the envelope itself. A workflow that fans out per row belongs in a row-level step; a workflow that needs one aggregate belongs in a bulk step.

`Add a step`

```bash
curl --request POST \
  --url "https://api.returning.ai/v1/workflow-builder/data-workflows/65f1a2b3c4d5e6f708192a3b/nodes" \
  --header "Authorization: Bearer <API_KEY>" \
  --header "Content-Type: application/json" \
  --header "If-Match: 162af81f5de4ff2b557a4087de9a9a73a0ed4e621f737787e35a93614cb50d1c" \
  --data '{
    "type": "platform-action",
    "name": "Read depositor",
    "position": {
      "after": "trigger"
    },
    "options": {
      "action": "get-user-fields",
      "selectUser": {
        "type": "email",
        "value": ["{{65f1a2b3c4d5e6f708192a41.data[0].email}}"]
      },
      "fields": [
        {
          "label": "Total XP",
          "field": "total_xp"
        }
      ]
    }
  }'
```

Then validate and dry-run with a sample envelope:

`Test a whole workflow`

```bash
curl --request POST \
  --url "https://api.returning.ai/v1/workflow-builder/data-workflows/65f1a2b3c4d5e6f708192a3b/test" \
  --header "Authorization: Bearer <API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{
    "mode": "dry-run",
    "triggerPayload": {
      "trigger_id": "65f1a2b3c4d5e6f708192a41",
      "metadata": {
        "batch_id": "batch-0001"
      },
      "data": [
        {
          "email": "trader@example.com",
          "deposit_id": "D-1",
          "net_deposit_delta": "500"
        }
      ]
    }
  }'
```

Readback: the trace resolves the row from the seeded payload. A literal expression in the trace means the binding is wrong; see [Bind step inputs](https://docs.returning.ai/workflow-builder/bind-step-inputs.md).

## 4. Deploy, then read a batch's status

`Deploy a workflow`

```bash
curl --request POST \
  --url "https://api.returning.ai/v1/workflow-builder/data-workflows/65f1a2b3c4d5e6f708192a3b/deploy" \
  --header "Authorization: Bearer <API_KEY>" \
  --header "If-Match: 68f78253046393414dec404e5442c140bb5db0393a7d1c389ff57bba71585501"
```

After the sender posts a batch, three surfaces answer three different questions:

| Surface | Question | Where |
| --- | --- | --- |
| Event ingest status | Did the platform receive this batch, and did the workflow run it? | [`GET /v1/data-workflows/event-ingests/status?trigger_id=<triggerId>&batch_id=<batchId>`](https://docs.returning.ai/api-reference/data-workflows/read-event-ingest-lifecycle-status-by-trigger-and-batch-id.md) |
| [Bulk update callback](https://docs.returning.ai/broker-integrations/workflow-webhooks.md#bulk-update-callback) | Did the resulting user update finish, and how many rows succeeded? | A URL the broker controls, configured on the bulk step |
| Failure webhook | Did a run end in error? | The workflow's `outgoingWebhookUrl` |

The status read needs a community API key with the `dataWorkflows` permission. It is a separate permission from `workflowBuilder`, and it is the one place `dataWorkflows` still applies.

Meanings that matter:

| Status | Means |
| --- | --- |
| `pending` or `queued` | Accepted, waiting. Minutes of wait are normal for a scheduled dispatch. |
| `processed` | The workflow *ran*. It does not mean every step succeeded, and it does not mean balances changed. |
| `skipped` | A duplicate batch id, or every row was a duplicate or had no record id. |
| `failed` | Rejected before running, for example a batch id missing from the configured path. |
| `orphaned` | No deployed workflow matched the trigger. |

Read the run's own result with [`GET /{workflowId}/runs`](https://docs.returning.ai/api-reference/workflow-builder/list-the-runs-of-a-workflow.md) when you need to know whether steps succeeded. `processed` and a run `error` can both be true.

Read the newest status before an exact retry: a retry shows as `skipped`, which hides the original result.

## 5. Prove the batch end to end

1. [`GET /{workflowId}/runs`](https://docs.returning.ai/api-reference/workflow-builder/list-the-runs-of-a-workflow.md) for the run and [`GET /runs/{runId}`](https://docs.returning.ai/api-reference/workflow-builder/get-one-run.md) for the step results.
2. For a bulk step, the bulk update job: status, then per-row details.
3. The member's own field value.
4. Your own ledger, to confirm you acknowledged the batch once.

A `2xx` from the sender's post proves only that the request was accepted. An ingest row proves it was routed. A run proves it executed. Only the member's value proves the reward.

## Failure handling

| What you see | What it means | What to do |
| --- | --- | --- |
| Ingest `failed` with a batch id path message | The batch id was not at the configured root path | Fix the sender, resend under a new batch id. |
| Ingest `skipped` | Duplicate batch, or all rows duplicated | Nothing to do unless the data is genuinely new. |
| Ingest `processed`, run `error` | The workflow ran and a step failed | Read the run detail, fix the step. |
| Status read `404` right after a `2xx` ingest | The batch id could not be read, so the row cannot be found by that id | Check the envelope, then ask for the batch on the Event Ingests board. |
| Bulk job `Failed`, every row `success` | The job failed as a whole and nothing was applied | Check the CSV header names the community's identifier field, then send a new batch. |
| No ingest row at all | The request never reached the platform | Confirm the exact URL the sender posted to resolves, and ask for the sender's exact request. |

## Cleanup

Undeploy when the pipeline should stop, then delete. A deleted workflow takes its run history with it, and ingest rows remain readable only through the sender's batch ids for a short window, so keep your own record of what a canary batch proved.

## What the API does not cover

- Sending a batch. The sender posts the envelope; there is no Workflow Builder route that submits one.
- The bulk job's own status and per-row details, which are separate platform endpoints.
- Reading ingest payloads in bulk. The Event Ingests board and its routes are a different surface, and the public status endpoint reads one batch summary.
- Triggering a dispatch by hand.

## Endpoints on this page

- POST [Create a workflow](https://docs.returning.ai/api-reference/workflow-builder/create-a-workflow.md) `/`
- PUT [Create or replace the trigger](https://docs.returning.ai/api-reference/workflow-builder/create-or-replace-the-trigger.md) `/{workflowId}/trigger`
- POST [Add a step](https://docs.returning.ai/api-reference/workflow-builder/add-a-step.md) `/{workflowId}/nodes`
- POST [Test a whole workflow](https://docs.returning.ai/api-reference/workflow-builder/test-a-whole-workflow.md) `/{workflowId}/test`
- POST [Deploy a workflow](https://docs.returning.ai/api-reference/workflow-builder/deploy-a-workflow.md) `/{workflowId}/deploy`
- GET [List step types and trigger types](https://docs.returning.ai/api-reference/workflow-builder/list-step-types-and-trigger-types.md) `/catalog`
- GET [Read event-ingest lifecycle status by trigger and batch ID](https://docs.returning.ai/api-reference/data-workflows/read-event-ingest-lifecycle-status-by-trigger-and-batch-id.md) `/v1/data-workflows/event-ingests/status`
- GET [List the runs of a workflow](https://docs.returning.ai/api-reference/workflow-builder/list-the-runs-of-a-workflow.md) `/{workflowId}/runs`
- GET [Get one run](https://docs.returning.ai/api-reference/workflow-builder/get-one-run.md) `/runs/{runId}`

Paths that start with `/v1/` are full paths; the rest are relative to `https://api.returning.ai/v1/workflow-builder/data-workflows`.
