# Workflows (/docs/reference/api/workflows)



Workflows API [#workflows-api]

This API manages SQL workflows in RawTree Cloud and RawTree BYOC, where enabled.
RawTree executes the SQL. The service manages schedules, runs, and result delivery.

Workflows run SQL on demand or on a schedule. They send read-query results to
HTTP, table, or Slack sinks. See the
[workflow guide](/docs/guides/transform/workflows) for SQL behavior and
delivery guarantees.

All workflow endpoints require `organization` and `cluster` query parameters,
using their names, and an admin API key, a user session bearer token, or an OAuth
token with `full_access`. Admin API keys can read and manage workflows only in
their bound organization and cluster; other API key permissions, including custom
database roles, are not supported. Organization members can read workflows, runs,
logs, and metrics; organization administrators can create, run, update, and delete workflows, and cancel runs.

Workflows belong to the organization and execute with organization credentials.
Expiring or revoking the API key used to create a workflow prevents further API
access with that key but does not stop the workflow. Pause or delete the workflow
to stop future scheduled executions.

A disabled workflow service returns `503`.

GET `/v1/workflows` [#get-v1workflows]

List workflows in the selected organization and cluster.

```json
{ "workflows": [] }
```

Use an API key, a permitted workload token, or a user bearer token. Database-role keys support only the operations listed in [Authentication](/docs/reference/authentication#existing-database-roles).

Parameters:

| Field          | Location | Type   | Required | Notes |
| -------------- | -------- | ------ | -------- | ----- |
| `organization` | query    | string | Yes      | —     |
| `cluster`      | query    | string | Yes      | —     |

Request:

```bash
# List workflows in an organization and cluster.
curl -X GET "https://api.rawtree.com/v1/workflows?organization=team_alpha&cluster=production" \
  -H "Authorization: Bearer $RAWTREE_API_KEY"
```

Expected response: **HTTP 200**.

```json
{
  "workflows": [
    {
      "id": "11111111-1111-4111-8111-111111111111",
      "name": "event-count",
      "query": {
        "database": "analytics",
        "sql": "SELECT count() AS total FROM events"
      },
      "schedule": null,
      "sinks": [],
      "revision": 1,
      "created_at": "2026-11-09T10:00:00Z",
      "updated_at": "2026-11-09T10:00:00Z"
    }
  ]
}
```

Errors: `403`, `503`. See [API errors](/docs/reference/errors).

POST `/v1/workflows` [#post-v1workflows]

Create a workflow. Returns `201` with the saved workflow, including its `id`,
`revision`, `created_at`, `updated_at`, and sink metadata.

```json
{
  "name": "high-volume-customers",
  "description": "Customers with more than 100 events",
  "query": {
    "database": "default",
    "sql": "SELECT customer_id, count() AS total FROM events GROUP BY customer_id HAVING total > 100"
  },
  "schedule": {
    "cron": "*/5 * * * *",
    "enabled": true
  },
  "sinks": []
}
```

The required `query` object contains `database` and `sql`. Responses use the same
shape. Top-level `database` and `sql` fields are rejected.

`schedule` controls automatic executions. Set it to `null` for a manual-only
workflow. When omitted, it defaults to running every second:
`{"cron": "* * * * * *", "enabled": true}`. Within `schedule`, `cron` is
required and `enabled` defaults to `true`. `sinks` defaults to an empty list.

`cron` uses UTC. It accepts five fields (`minute hour day-of-month month
day-of-week`) or six fields with `second` first. Each field accepts `*`, a
number, a range such as `9-17`, a step such as `*/15` or `5/10`, or a
comma-separated list.

Day of week uses `0`–`7`; `0` and `7` mean Sunday. If both day fields are
restricted, a run requires both to match. Names, shortcuts such as `@hourly`,
and `?`, `L`, or `#` return `422`. The workflow service normalizes spaces and limits the
normalized expression to 1,024 characters. See [schedule a workflow](/docs/guides/transform/workflows#schedule-a-workflow)
for examples and missed-run behavior.

Each cluster supports up to 100 workflows, including paused workflows. Names are
trimmed and lowercased and must be unique in the cluster. The optional
`description` is trimmed, accepts up to 1,000 characters, and is `null` when
omitted or blank.

Use an API key, a permitted workload token, or a user bearer token. Database-role keys support only the operations listed in [Authentication](/docs/reference/authentication#existing-database-roles).

Parameters:

| Field          | Location | Type                     | Required | Notes                                                                                           |
| -------------- | -------- | ------------------------ | -------- | ----------------------------------------------------------------------------------------------- |
| `organization` | query    | string                   | Yes      | —                                                                                               |
| `cluster`      | query    | string                   | Yes      | —                                                                                               |
| `name`         | body     | string                   | Yes      | —                                                                                               |
| `description`  | body     | string or null           | No       | Optional free-form text. Surrounding whitespace is trimmed and blank values are stored as null. |
| `query`        | body     | WorkflowQuery            | Yes      | —                                                                                               |
| `schedule`     | body     | null or WorkflowSchedule | No       | —                                                                                               |
| `sinks`        | body     | array of SinkInput       | No       | —                                                                                               |

Request:

```bash
# Create a scheduled SQL workflow.
curl -X POST "https://api.rawtree.com/v1/workflows?organization=team_alpha&cluster=production" \
  -H "Authorization: Bearer $RAWTREE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "event-count",
  "query": {
    "database": "analytics",
    "sql": "SELECT count() AS total FROM events"
  },
  "schedule": null,
  "sinks": []
}'
```

Expected response: **HTTP 201**.

```json
{
  "id": "11111111-1111-4111-8111-111111111111",
  "name": "event-count",
  "query": {
    "database": "analytics",
    "sql": "SELECT count() AS total FROM events"
  },
  "schedule": null,
  "sinks": [],
  "revision": 1,
  "created_at": "2026-11-09T10:00:00Z",
  "updated_at": "2026-11-09T10:00:00Z"
}
```

Errors: `400`, `403`, `409`, `422`, `503`. See [API errors](/docs/reference/errors).

GET `/v1/workflows/{workflow_id}` [#get-v1workflowsworkflow_id]

Get a workflow and its schedule state. HTTP sink responses include `settings.url`
and `settings.header_names`. The URL is `null` if its encrypted configuration
cannot be read; the workflow and sink metadata remain available for repair.
Header values are write-only and are never returned.
`schedule` is `null` for manual-only workflows. Otherwise it contains `cron`,
`enabled`, and `next_run_at`, the next scheduled evaluation time in
RFC 3339 format. `next_run_at` is `null` while paused or when no future time is
available. A successful create or update can also return `null` if schedule
metadata could not be refreshed.

Use an API key, a permitted workload token, or a user bearer token. Database-role keys support only the operations listed in [Authentication](/docs/reference/authentication#existing-database-roles).

Parameters:

| Field          | Location | Type   | Required | Notes |
| -------------- | -------- | ------ | -------- | ----- |
| `organization` | query    | string | Yes      | —     |
| `cluster`      | query    | string | Yes      | —     |

Request:

```bash
# Get a workflow and its sink configuration.
curl -X GET "https://api.rawtree.com/v1/workflows/11111111-1111-4111-8111-111111111111?organization=team_alpha&cluster=production" \
  -H "Authorization: Bearer $RAWTREE_API_KEY"
```

Expected response: **HTTP 200**.

```json
{
  "id": "11111111-1111-4111-8111-111111111111",
  "name": "event-count",
  "query": {
    "database": "analytics",
    "sql": "SELECT count() AS total FROM events"
  },
  "schedule": null,
  "sinks": [],
  "revision": 1,
  "created_at": "2026-11-09T10:00:00Z",
  "updated_at": "2026-11-09T10:00:00Z"
}
```

Errors: `403`, `404`, `503`. See [API errors](/docs/reference/errors).

PATCH `/v1/workflows/{workflow_id}` [#patch-v1workflowsworkflow_id]

Update the supplied fields and return the saved workflow. Omitted fields retain
their existing values.
Omitting `query` preserves the saved query. Within `query`, omitted `database`
or `sql` fields retain their values; for example, `{"query":{"sql":"SELECT 1"}}`
changes only the SQL.
Within `schedule`, omitted fields retain their values:
`{"schedule": {"enabled": false}}` pauses automatic runs and
`{"schedule": {"enabled": true}}` resumes them and keeps the cron. Set
`schedule` to `null` for manual-only execution. To schedule it again, supply a
`cron`; `enabled` then defaults to `true`. Pausing does not prevent explicit runs.
Set `description` to `null` or blank text to clear it.

```json
{ "schedule": { "cron": "*/10 * * * * *", "enabled": false } }
```

Supplying `sinks` replaces the full list. Preserve existing sink IDs when editing;
omit `id` for new sinks. An empty list removes every sink. Each sink has a `type`
and a typed `settings` object: HTTP settings accept `url` and `headers`; table
settings require `database` and `table`; Slack settings require
`integration_id` and exactly one of `channel_id` or `channel_name`. HTTP
responses expose `settings.url`, `settings.url_configured`, and
`settings.header_names`, never header values. Slack responses expose
`settings.integration_id`, `settings.channel_id`, and `settings.channel_name`.
Flattened fields are rejected.
See [sink configuration](/docs/guides/transform/workflows#configure-sinks)
for HTTP credentials and table sinks, and
[Post results to Slack](/docs/guides/transform/workflows#post-results-to-slack)
for connecting a Slack workspace.

Use an API key, a permitted workload token, or a user bearer token. Database-role keys support only the operations listed in [Authentication](/docs/reference/authentication#existing-database-roles).

Parameters:

| Field          | Location | Type                           | Required | Notes                                                           |
| -------------- | -------- | ------------------------------ | -------- | --------------------------------------------------------------- |
| `organization` | query    | string                         | Yes      | —                                                               |
| `cluster`      | query    | string                         | Yes      | —                                                               |
| `name`         | body     | string or null                 | No       | —                                                               |
| `description`  | body     | string or null                 | No       | Omit to preserve the description; null or blank text clears it. |
| `query`        | body     | null or UpdateWorkflowQuery    | No       | —                                                               |
| `schedule`     | body     | null or UpdateWorkflowSchedule | No       | —                                                               |
| `sinks`        | body     | array or null                  | No       | —                                                               |

Request:

```bash
# Update a workflow, including its schedule and sinks.
curl -X PATCH "https://api.rawtree.com/v1/workflows/11111111-1111-4111-8111-111111111111?organization=team_alpha&cluster=production" \
  -H "Authorization: Bearer $RAWTREE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "schedule": {
    "cron": "*/5 * * * *",
    "enabled": true
  }
}'
```

Expected response: **HTTP 200**.

```json
{
  "id": "11111111-1111-4111-8111-111111111111",
  "name": "event-count",
  "query": {
    "database": "analytics",
    "sql": "SELECT count() AS total FROM events"
  },
  "schedule": {
    "cron": "*/5 * * * *",
    "enabled": true
  },
  "sinks": [],
  "revision": 2,
  "created_at": "2026-11-09T10:00:00Z",
  "updated_at": "2026-11-09T10:00:00Z"
}
```

Errors: `400`, `403`, `404`, `409`, `422`, `503`. See [API errors](/docs/reference/errors).

DELETE `/v1/workflows/{workflow_id}` [#delete-v1workflowsworkflow_id]

Delete the workflow and its schedule. Returns `204` with no response body.

Use an API key, a permitted workload token, or a user bearer token. Database-role keys support only the operations listed in [Authentication](/docs/reference/authentication#existing-database-roles).

Parameters:

| Field          | Location | Type   | Required | Notes |
| -------------- | -------- | ------ | -------- | ----- |
| `organization` | query    | string | Yes      | —     |
| `cluster`      | query    | string | Yes      | —     |

Request:

```bash
# Delete a workflow and its schedule.
curl -X DELETE "https://api.rawtree.com/v1/workflows/11111111-1111-4111-8111-111111111111?organization=team_alpha&cluster=production" \
  -H "Authorization: Bearer $RAWTREE_API_KEY"
```

Expected response: **HTTP 204**.

The response body is empty.

Errors: `403`, `404`, `503`. See [API errors](/docs/reference/errors).

POST `/v1/workflows/{workflow_id}/sinks` [#post-v1workflowsworkflow_idsinks]

Add one sink without resending the others. The body is a sink as in the
`sinks` list, without `id`; the new sink goes last. Returns `201` with the saved
sink. A workflow holds up to five sinks.

```json
{ "type": "table", "settings": { "database": "analytics", "table": "alerts" } }
```

Use an API key, a permitted workload token, or a user bearer token. Database-role keys support only the operations listed in [Authentication](/docs/reference/authentication#existing-database-roles).

Parameters:

| Field          | Location | Type   | Required | Notes |
| -------------- | -------- | ------ | -------- | ----- |
| `organization` | query    | string | Yes      | —     |
| `cluster`      | query    | string | Yes      | —     |

Request:

```bash
# Add a sink to a workflow.
curl -X POST "https://api.rawtree.com/v1/workflows/11111111-1111-4111-8111-111111111111/sinks?organization=team_alpha&cluster=production" \
  -H "Authorization: Bearer $RAWTREE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "type": "table",
  "settings": {
    "database": "analytics",
    "table": "event_counts"
  }
}'
```

Expected response: **HTTP 201**.

```json
{
  "id": "11111111-1111-4111-8111-111111111111",
  "type": "table",
  "settings": {
    "database": "analytics",
    "table": "event_counts"
  }
}
```

Errors: `400`, `403`, `404`, `409`, `422`, `503`. See [API errors](/docs/reference/errors).

PATCH `/v1/workflows/{workflow_id}/sinks/{sink_id}` [#patch-v1workflowsworkflow_idsinkssink_id]

Change one sink and return it. Send the `settings` to change; omitted settings
keep their stored values. HTTP `headers` work as in a `PATCH` of `sinks`: the
map lists every header the sink keeps, `null` keeps a header's stored value,
and omitted headers are removed. `type` is optional and must match the sink's
type; to change the type, delete the sink and add a new one.

This keeps `Authorization`, adds `X-Region`, and removes any other header:

```json
{ "settings": { "headers": { "Authorization": null, "X-Region": "eu" } } }
```

Use an API key, a permitted workload token, or a user bearer token. Database-role keys support only the operations listed in [Authentication](/docs/reference/authentication#existing-database-roles).

Parameters:

| Field          | Location | Type                                                                      | Required | Notes |
| -------------- | -------- | ------------------------------------------------------------------------- | -------- | ----- |
| `organization` | query    | string                                                                    | Yes      | —     |
| `cluster`      | query    | string                                                                    | Yes      | —     |
| `type`         | body     | null or SinkType                                                          | No       | —     |
| `settings`     | body     | HttpSinkSettingsInput or TableSinkSettingsPatch or SlackSinkSettingsPatch | Yes      | —     |

Request:

```bash
# Change one sink of a workflow; omitted settings keep their stored values.
curl -X PATCH "https://api.rawtree.com/v1/workflows/11111111-1111-4111-8111-111111111111/sinks/11111111-1111-4111-8111-111111111111?organization=team_alpha&cluster=production" \
  -H "Authorization: Bearer $RAWTREE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "settings": {
    "table": "event_counts_archive"
  }
}'
```

Expected response: **HTTP 200**.

```json
{
  "id": "11111111-1111-4111-8111-111111111111",
  "type": "table",
  "settings": {
    "database": "analytics",
    "table": "event_counts_archive"
  }
}
```

Errors: `400`, `403`, `404`, `409`, `422`, `503`. See [API errors](/docs/reference/errors).

DELETE `/v1/workflows/{workflow_id}/sinks/{sink_id}` [#delete-v1workflowsworkflow_idsinkssink_id]

Remove one sink. Returns `204` with no response body.

Sink changes through these routes stop queued and active runs of the previous
definition, as a `PATCH` of `sinks` does. Each route changes only its own sink,
so other sinks stay as saved. A sink `PATCH` returns `409` if the workflow
changes while it is applied.

Use an API key, a permitted workload token, or a user bearer token. Database-role keys support only the operations listed in [Authentication](/docs/reference/authentication#existing-database-roles).

Parameters:

| Field          | Location | Type   | Required | Notes |
| -------------- | -------- | ------ | -------- | ----- |
| `organization` | query    | string | Yes      | —     |
| `cluster`      | query    | string | Yes      | —     |

Request:

```bash
# Remove one sink from a workflow.
curl -X DELETE "https://api.rawtree.com/v1/workflows/11111111-1111-4111-8111-111111111111/sinks/11111111-1111-4111-8111-111111111111?organization=team_alpha&cluster=production" \
  -H "Authorization: Bearer $RAWTREE_API_KEY"
```

Expected response: **HTTP 204**.

The response body is empty.

Errors: `403`, `404`, `409`, `503`. See [API errors](/docs/reference/errors).

POST `/v1/workflows/{workflow_id}/runs` [#post-v1workflowsworkflow_idruns]

Start an execution using the saved definition without changing its schedule.
Requires an organization administrator or admin API key. No request body is
required. Returns `202` with `id`, `workflow_id`, `query_id`, and
`source: "manual"`.
The optional `Idempotency-Key` header accepts 1–128 visible ASCII characters
without spaces. Reusing a key recovers the same execution while Temporal retains
it. See [explicit runs](/docs/guides/transform/workflows#run-a-workflow-explicitly) for
revision checks, overlap, and delivery semantics.

Use an API key, a permitted workload token, or a user bearer token. Database-role keys support only the operations listed in [Authentication](/docs/reference/authentication#existing-database-roles).

Parameters:

| Field             | Location | Type           | Required | Notes                                                                                                                                                 |
| ----------------- | -------- | -------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Idempotency-Key` | header   | string or null | No       | Optional retry key, 1–128 visible ASCII characters without spaces. Scoped to this workflow; deduplication lasts while Temporal retains the execution. |

Request:

```bash
# Execute a saved workflow once, independently of its schedule.
curl -X POST "https://api.rawtree.com/v1/workflows/11111111-1111-4111-8111-111111111111/runs?organization=team_alpha&cluster=production" \
  -H "Authorization: Bearer $RAWTREE_API_KEY"
```

Expected response: **HTTP 202**.

```json
{
  "id": "11111111-1111-4111-8111-111111111111",
  "workflow_id": "11111111-1111-4111-8111-111111111111",
  "query_id": "workflow-docs-run",
  "source": "manual"
}
```

Errors: `400`, `401`, `403`, `404`, `503`. See [API errors](/docs/reference/errors).

POST `/v1/workflows/{workflow_id}/runs/{run_id}/cancel` [#post-v1workflowsworkflow_idrunsrun_idcancel]

Request cancellation of one manual or scheduled execution. Requires an
organization administrator or admin API key, with the same `organization` and
`cluster` query parameters as the other workflow endpoints. No body is required.
Returns `202` with an empty body once cancellation is accepted.

```bash
curl -X POST \
  "$RAWTREE_URL/v1/workflows/$WORKFLOW_ID/runs/$RUN_ID/cancel?organization=$ORGANIZATION&cluster=$CLUSTER" \
  -H "Authorization: Bearer $RAWTREE_API_KEY"
```

Cancellation is asynchronous and requires a worker to process it. Use the runs
list to inspect the eventual status. It preserves execution history, other
concurrent runs, and future scheduled executions. It does not roll back completed
writes or recall deliveries already handed off to sinks. Query cleanup can take
time, especially when a database replica is unavailable.

Repeating cancellation is safe: a pending request or an already canceled run
returns `202`. A run that has otherwise finished returns `409`. Unknown runs,
runs belonging to another workflow, and deleted workflows return `404`. Because
run lookup uses Temporal visibility, a newly started run may briefly return
`404`; retry once it appears in the runs list. A `503` means acceptance could not
be confirmed; retry cancellation for the same run.

Use an API key, a permitted workload token, or a user bearer token. Database-role keys support only the operations listed in [Authentication](/docs/reference/authentication#existing-database-roles).

Request:

```bash
# Request cancellation of one manual or scheduled run without changing its schedule.
# No body is required. Acceptance is asynchronous and does not undo completed writes.
# Recently started runs may not yet be visible. Repeating a request is safe;
# already canceled runs return 202 and other finished runs return 409.
curl -X POST "https://api.rawtree.com/v1/workflows/11111111-1111-4111-8111-111111111111/runs/11111111-1111-4111-8111-111111111111/cancel?organization=team_alpha&cluster=production" \
  -H "Authorization: Bearer $RAWTREE_API_KEY"
```

Expected response: **HTTP 202**.

The response body is empty.

Errors: `400`, `401`, `403`, `404`, `409`, `503`. See [API errors](/docs/reference/errors).

GET `/v1/workflows/{workflow_id}/runs` [#get-v1workflowsworkflow_idruns]

List manual and scheduled executions retained by Temporal for this workflow.
`limit` defaults to `20` and accepts 1–100. This is a per-page limit, not a total
history limit. Pass `next_cursor` as `cursor` to request another page for the same
workflow. The page size may change between requests. `next_cursor: null` means
there are no more pages; an empty page with a cursor can still be continued.

```json
{
  "runs": [
    {
      "id": "01a10ffc-b4f7-7871-859c-6353f93d875f",
      "workflow_id": "d668a6c1-1904-4a27-b113-ef54b5435483",
      "query_id": "workflow:d668a6c1-1904-4a27-b113-ef54b5435483:01a10ffc-b4f7-7871-859c-6353f93d875f",
      "source": "manual",
      "status": "running",
      "started_at": "2026-10-06T10:32:00Z",
      "finished_at": null,
      "deliveries": [
        {
          "sink_id": "5b0f2f0e-7c8e-4d4b-9a2b-6f1d0c3e8a71",
          "status": "delivering"
        }
      ]
    }
  ],
  "next_cursor": null
}
```

`id` is the Temporal run ID and matches the ID returned by POST. `query_id` is
the RawTree query ID for the run, in the form
`workflow:{workflow_id}:{run_id}`. Search `system.query_log` for this ID to
inspect the query when it has executed. `source` is `manual` or `scheduled`.
`status` is `running`, `completed`, `failed`, `canceled`, `terminated`,
`continued_as_new`, `timed_out`, or `paused`. Timestamps use RFC 3339 in UTC;
`finished_at` is null until the execution closes.

Active runs appear first by descending start time, then finished runs by
descending finish time. Temporal visibility is eventually consistent: recent
starts and status changes may appear after a delay. Pagination reflects live
history, so refresh from the first page to see current state. Runs remain
available after pausing or removing a schedule, until Temporal expires their
history. Deleting the saved workflow makes its runs endpoint return `404`.

Sinks receive rows asynchronously, and delivery to a sink is retried for up to an
hour. A run stays `running` until every sink it sent rows to reports how
delivery ended. It is `completed` only when every sink received its rows, and
`failed` if a sink rejected them or kept failing for an hour. If a sink's
outcome is still unknown when the 65-minute delivery confirmation window closes,
the run fails: the rows may have reached the sink, but the service could not confirm
it. Cancelling a run that
is waiting for its sinks closes it as `canceled`; rows already handed off may
still be delivered.

`deliveries` lists each sink the run sent rows to, ordered by sink ID, and is
empty when the run sent none. Each `status` is `delivering`, `delivered`,
`failed`, or `delivery_unknown`. These statuses describe this run's delivery, not
the sink's configuration. Inspect the workflow logs for SQL outcomes and each
delivery attempt. Invalid limits or cursors return `400`; an unavailable
execution service returns `503`.

Use an API key, a permitted workload token, or a user bearer token. Database-role keys support only the operations listed in [Authentication](/docs/reference/authentication#existing-database-roles).

Request:

```bash
# List manual and scheduled executions retained by Temporal.
# Active executions come first, then finished executions by descending finish
# time. Visibility is eventually consistent. A run stays `running` until every
# sink it handed rows to reports its delivery, and fails if any delivery
# failed or is unconfirmed after 65 minutes. `deliveries` shows each sink's
# progress; per-attempt details are in logs.
curl -X GET "https://api.rawtree.com/v1/workflows/11111111-1111-4111-8111-111111111111/runs?organization=team_alpha&cluster=production" \
  -H "Authorization: Bearer $RAWTREE_API_KEY"
```

Expected response: **HTTP 200**.

```json
{
  "runs": [],
  "next_cursor": null
}
```

Errors: `400`, `401`, `403`, `404`, `503`. See [API errors](/docs/reference/errors).

GET `/v1/workflows/{workflow_id}/logs` [#get-v1workflowsworkflow_idlogs]

List execution and delivery history. Optional `from` (inclusive) and `to`
(exclusive) are Unix milliseconds and default to the last 24 hours. Ranges can
cover up to seven days. `limit` defaults to `100` and accepts 1–200.

The response contains `logs`, `from`, `to`, and `next_cursor`. Each log includes
`event_type` (for example, `workflow.evaluated`) and `workflow_revision`.
Delivery records identify the sink with `sink_id` and `sink_type`.
Pass the returned `from`, `to`, and `next_cursor` as `cursor` to fetch older logs
within the same window. History is best-effort observability data.

Use an API key, a permitted workload token, or a user bearer token. Database-role keys support only the operations listed in [Authentication](/docs/reference/authentication#existing-database-roles).

Parameters:

| Field          | Location | Type            | Required | Notes                                                       |
| -------------- | -------- | --------------- | -------- | ----------------------------------------------------------- |
| `organization` | query    | string          | Yes      | —                                                           |
| `cluster`      | query    | string          | Yes      | —                                                           |
| `from`         | query    | integer or null | No       | Inclusive Unix milliseconds. Defaults to the last 24 hours. |
| `to`           | query    | integer or null | No       | Exclusive Unix milliseconds.                                |
| `cursor`       | query    | string or null  | No       | —                                                           |
| `limit`        | query    | integer or null | No       | —                                                           |

Request:

```bash
# List workflow execution and sink delivery logs.
curl -X GET "https://api.rawtree.com/v1/workflows/11111111-1111-4111-8111-111111111111/logs?organization=team_alpha&cluster=production" \
  -H "Authorization: Bearer $RAWTREE_API_KEY"
```

Expected response: **HTTP 200**.

```json
{
  "logs": [],
  "from": 1794214800000,
  "to": 1794218400000
}
```

Errors: `400`, `403`, `404`, `503`. See [API errors](/docs/reference/errors).

Sink delivery metrics [#sink-delivery-metrics]

Read sink delivery metrics through [`GET /v1/metrics`](/docs/reference/api/metrics#get-v1metrics).
Each observation contains the organization, cluster, workflow
(`rawtree.workflow.id`), and sink (`rawtree.workflow.sink.id`) identities.

| Metric                               | Meaning                                                  | Unit      |
| ------------------------------------ | -------------------------------------------------------- | --------- |
| `rawtree.workflow.sink.errors`       | Terminal delivery failures; retry attempts are excluded  | `{error}` |
| `rawtree.workflow.sink.written_rows` | Rows in successful sink deliveries, including HTTP sinks | `{row}`   |

These are monotonic **delta sums**. Use `aggregation=sum` to add deliveries
within each bucket. Do not use `function=increase`, which requires cumulative
counters. Filter by workflow ID and group by sink ID:

```sh
curl -X GET -G "https://api.rawtree.com/v1/metrics" \
  -H "Authorization: Bearer $RAWTREE_SESSION_TOKEN" \
  --data-urlencode "organization=$RAWTREE_ORGANIZATION" \
  --data-urlencode "cluster=$RAWTREE_CLUSTER" \
  --data-urlencode 'metric=rawtree.workflow.sink.errors,rawtree.workflow.sink.written_rows' \
  --data-urlencode "filters={\"rawtree.workflow.id\":\"$WORKFLOW_ID\"}" \
  --data-urlencode 'aggregation=sum' \
  --data-urlencode 'granularity_seconds=60' \
  --data-urlencode 'group_by=rawtree.workflow.sink.id' \
  --data-urlencode 'window_seconds=3600'
```

Metric export is buffered and best effort; it does not block sink delivery.
History starts when the deployment begins emitting these metrics. Existing logs
are not backfilled into metrics. Workflow logs remain available for individual
attempts, failure details, and links to runs. The sink metrics endpoint
`/v1/workflows/{workflow_id}/sinks/metrics` has been removed.
