---
title: Advanced: run a schedule via API
description: Advanced path — authenticate, invoke POST /v1/actions/{action_id}/runs or the {handle}/{slug} vanity path, and handle every error the invoke path returns.
---

Most schedules should just run on a cadence — see
[Create a schedule](/agents/actions/create). This page is the advanced path: it
lets an external system, rather than a clock, decide when a run happens.

Start a run from your own service with the API-call trigger. This page covers
the invoke contract only; see [Runs and results](/agents/actions/runs) for
polling and result shapes. The wire namespace is `/v1/actions` — see the
[Scheduled overview](/agents/actions) for how that name maps to the product.

Exact request and response schemas come from live OpenAPI
(`https://api.sume.com/reference/json`). The tables here are a readable summary,
not a second schema.

## Prerequisites

Every run request needs all three:

1. The schedule's `status` is `active`.
2. The schedule's `api_trigger_enabled` is `true`.
3. Your API key carries `actions:read` and `actions:write`.

## Scopes

| Scope | Needed for |
|---|---|
| `actions:read` | List and read Actions, read and list runs. |
| `actions:write` | Create a run, cancel a run. |

**Keys created before the API-call trigger shipped do not carry these scopes.**
An older key fails every run request with `403 insufficient_scope`, and scopes
cannot be added to an existing key. Create a new key at
[API Keys](https://www.sume.com/dashboard/api-keys) and rotate to it — see
[Authentication](/authentication).

Service-account keys cannot create Action runs. They fail with
`403 insufficient_scope` and `details.reason` of
`service_account_action_runs_unsupported`.

## Invoke

```bash
export SUME_API_KEY="sume_live_..."
export ACTION_ID="aut_..."

curl -sS -X POST "https://api.sume.com/v1/actions/$ACTION_ID/runs" \
  -H "Authorization: Bearer $SUME_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{}'
```

An accepted run returns `202` with a run receipt:

```json
{
  "data": {
    "id": "run_...",
    "object": "action.run",
    "action": { "id": "aut_...", "title": "Weekly product teaser", "trigger_type": "api" },
    "status": "queued",
    "trigger": { "source": "api", "idempotency_key": "..." },
    "status_url": "https://api.sume.com/v1/action-runs/run_.../status",
    "result_url": "https://api.sume.com/v1/action-runs/run_.../result",
    "cancel_url": "https://api.sume.com/v1/action-runs/run_.../cancel",
    "cancelable": true,
    "next_action": "poll_status",
    "idempotency_hit": false
  }
}
```

## Vanity URLs

An Action can also be invoked by its owner's handle and its own slug:

```bash
curl -sS -X POST "https://api.sume.com/v1/actions/chasehuh/k-ugc/runs" \
  -H "Authorization: Bearer $SUME_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{}'
```

Request body, headers, scopes, idempotency, spend caps and the run receipt are
identical to the opaque form — the vanity path resolves to the same Action and
runs the same pipeline. `action.id` in the receipt is always the opaque
`aut_…` id.

`GET /v1/actions/{handle}/{slug}` and `GET /v1/actions/{handle}/{slug}/runs`
work the same way.

**Store the opaque id, not the vanity URL.** Renaming your handle or the
Action's slug changes the vanity path; `aut_…` never changes. A renamed handle
keeps resolving for 90 days, which is a migration window, not a guarantee.

`PublicAction` exposes both so you can choose:

| Field | Meaning |
|---|---|
| `invoke_url` | Opaque path. Always present, always permanent. |
| `slug` | The Action's URL segment, or `null` on Actions created before slugs existed. |
| `handle` | The owner's current handle, when one is resolvable. |
| `vanity_invoke_url` | The `{handle}/{slug}` path, or `null` when either half is unknown. |

Slugs are lowercase alphanumerics separated by single hyphens, 2–64 characters,
and unique within your account. `runs` is reserved.

An unknown handle, an unknown slug, and a handle you do not own all return the
same `404 action_not_found`.

Actions owned by a team workspace are not reachable over the public API yet, by
either path.

## Request body

The body accepts these properties and nothing else. Unknown properties are
rejected with `400`.

| Field | Type | Default | Notes |
|---|---|---|---|
| `input` | object | `{}` | Caller data for this run. Max 64 properties, max 2097152 UTF-8 bytes (2 MiB). |
| `on_active_run` | `skip` or `reject` | `skip` | What to do when a run is already active. Format runs default to **`allow`** instead — see [Calling a Format](/formats/call#request-body). |
| `generation_spend_cap_usd` | number ≥ 0 | Action cap | Clamped to `min(request, Action cap)`. Cannot raise the cap. |
| `primary_output_key` | string ≤ 64 | Action default | Selects which output key becomes `primary_output_url`. |
| `output_schema` | `{ name, strict, schema }` | Action default | Per-request output schema override. See below. |
| `response_format` | `{ type: "json_schema", json_schema }` | — | Documented OpenAI-shaped alias for `output_schema`. |
| `communication.mode` | `async` or `webhook` | `async` | Descriptive. `webhook_url` is what arms delivery. |
| `communication.webhook_url` | HTTPS URI ≤ 2048 | — | Terminal delivery target. See [Webhooks](#webhooks) for current availability. |

## Overriding the output schema

`output_schema` overrides whatever the Action binds, for this run only. The
receipt reports `output_schema.source: "request_override"`.

```json
{
  "input": { "product_name": "Aurora Headphones" },
  "output_schema": {
    "name": "sume/action-image-v1",
    "strict": true,
    "schema": {
      "type": "object",
      "additionalProperties": false,
      "required": ["caption", "image"],
      "properties": {
        "caption": { "type": ["string", "null"] },
        "image": { "$ref": "SumeMediaFile#" }
      }
    }
  },
  "primary_output_key": "image"
}
```

`output_schema.name` is echoed back verbatim on the receipt. Sume only rewrites
it internally when it calls the structuring model, which accepts a narrower
character set; that rewrite is never visible in the API.

The schema must follow the strict subset described in
[Create a schedule](/agents/actions/create#5-bind-an-output-schema-optional).
A schema outside it is rejected before any run starts:

```json
{
  "error": {
    "code": "output_schema_invalid",
    "message": "output_schema is not a satisfiable strict schema.",
    "details": {
      "violations": [
        {
          "path": "#/properties/caption",
          "rule": "required_completeness",
          "message": "Property \"caption\" must be listed in required (use a nullable type for optional values)."
        }
      ]
    },
    "next_action": "fix_input"
  }
}
```

If you already speak OpenAI Structured Outputs, `response_format` is accepted as
an alias and normalized into `output_schema`:

```json
{
  "response_format": {
    "type": "json_schema",
    "json_schema": { "name": "sume/action-image-v1", "strict": true, "schema": { } }
  }
}
```

Sending both `output_schema` and `response_format` is a `400 invalid_request`.

`output_schema` is part of the idempotency payload: replaying a key with a
different schema is an `409 idempotency_conflict`, not a silent replay of the
old receipt.

## How input reaches the Agent

`input` is serialized into a fenced JSON block and handed to the Agent as
**data, not instructions**. The Agent's behavior still comes from the Action's
saved instructions.

```json
{
  "input": {
    "product_name": "Aurora Headphones",
    "campaign": "summer-2026"
  }
}
```

Caller-supplied text is untrusted. Keep instructions authoritative and do not
design an Action that lets `input` redirect what it does. See
[Safe automation](/agents/safe-automation).

## Idempotency

Send an `Idempotency-Key` header (1–255 characters) on every run request.

- Replaying the same key with the same payload returns `200` with the original
  receipt and `idempotency_hit: true`. No second run starts.
- Reusing the same key with a different payload returns
  `409 idempotency_conflict`.
- Without a key, no replay protection is recorded and every request starts a new
  run.

## Response codes

| Status | Meaning |
|---|---|
| `202` | Run accepted and started. |
| `200` | Idempotency replay, **or** the run was skipped because another run was already active. |

`200` does not mean the work finished. Branch on the receipt's `status` field,
not on the HTTP status.

## Overlap behavior

Only one run of an Action is active at a time. `on_active_run` decides what
happens to a second request:

| Value | Result |
|---|---|
| `skip` (default) | `200` with a receipt whose `status` is `skipped` and `skip_reason` is `previous_run_active`. A run row is recorded. |
| `reject` | `409 action_run_in_progress`. No run is recorded. |

Use `reject` when a dropped trigger should surface as an error in your caller.
Use `skip` when overlapping triggers are expected and harmless.

## Errors

| Status | `error.code` | Cause | Fix |
|---|---|---|---|
| `400` | `output_schema_invalid` | `output_schema.schema` is outside the strict subset. `details.violations[]` names each rule. | Fix the schema. |
| `400` | `invalid_request` | `input` is not an object, exceeds 64 properties or 2097152 bytes; `generation_spend_cap_usd` is not a finite number ≥ 0; `webhook_url` is not a public HTTPS URL; Action instructions are empty. | Fix the request or the Action. |
| `401` | `unauthorized` | Missing, malformed, or revoked key. | Check the header; create a new key. |
| `403` | `insufficient_scope` | Key lacks `actions:write` (`details.required_scope`), or is a service-account key (`details.reason`). | Create a new dashboard key. |
| `404` | `action_not_found` | Unknown or archived Action, or it belongs to another workspace. | Check `action_id`. |
| `409` | `action_api_trigger_disabled` | `api_trigger_enabled` is `false`. | Enable the API call trigger. |
| `409` | `action_inactive` | Action `status` is `inactive`. | Set the Action Active. |
| `409` | `action_run_in_progress` | A run is active and `on_active_run` was `reject`. | Retry later, or use `skip`. |
| `409` | `idempotency_conflict` | Key reused with a different payload. | Use a new key. |
| `429` | rate limited | Public API rate limit. | Back off; see [Errors and rate limits](/workflows/errors-and-credits). |
| `503` | `studio_agent_upstream_unavailable` | The Agents control plane is unconfigured, unreachable, or returned a non-JSON response. `details.missing` reports `action_control_plane` when it is a configuration gap. | Retry; contact support if it persists. |

Errors use the standard Sume error envelope:

```json
{
  "error": {
    "code": "insufficient_scope",
    "message": "This API key does not have permission to access this endpoint.",
    "request_id": "req_...",
    "category": "validation",
    "stage": "validation",
    "retryable": false,
    "retry_after_seconds": null,
    "public_reason": "insufficient_scope",
    "next_action": "fix_input",
    "details": { "required_scope": "actions:write" }
  }
}
```

If you generate a client from the OpenAPI document, note that this route
declares `200`, `202`, `400`, `401`, `404`, `409`, `429`, and `500`. The `403`
and `503` responses above are raised by the auth and upstream layers and are not
in the declared response set, so a generated client may not model them. Handle
both.

Two envelope quirks worth knowing:

- `insufficient_scope` classifies as `category: "validation"`, not `auth`. Only
  `401` maps to the `auth` category.
- `studio_agent_upstream_unavailable` reports `retryable: false` and
  `next_action: "contact_support"` even though it describes an upstream
  condition. A bounded retry is still reasonable; escalate if it persists.

Always log `request_id` — it is the fastest way to get a run investigated.

## Webhooks

`communication.webhook_url` is a public HTTPS URL that receives one signed POST
carrying the terminal receipt. `callback_url` is an accepted alias. The full
contract — event names, envelope, signature, retry schedule — is on
[Run webhooks](/agents/run-webhooks).

Delivery is live on `api.dev.sume.com` and `api.sume.com`. Polling remains a
valid backup — see [Runs and results](/agents/actions/runs).

[Webhooks](/workflows/webhooks) documents webhooks for **generation jobs**, a
separate surface with its own `job.*` event set. The signature scheme is the
same, so one verifier covers both.

## Not available

- No MCP tool and no CLI command for Actions.
- No public endpoint to create, edit, or delete an Action — use the dashboard.
- No `/v1/action-runs/{run_id}/events` endpoint. The receipt's `events_url` is
  always `null`.

## Next

- [Runs and results](/agents/actions/runs)
- [Run webhooks](/agents/run-webhooks)
- [Safe automation](/agents/safe-automation)
