# Auto

URL: https://docs.hanzo.ai/docs/openapi/auto

> Workflows that run themselves, on a schedule or a webhook.

Also for this capability: **API** · [CLI](https://docs.hanzo.ai/docs/cli/auto) · [MCP](https://docs.hanzo.ai/docs/mcp-tools/auto) · [SDKs](https://docs.hanzo.ai/docs/sdks)

Workflows that run themselves, on a schedule or a webhook.

> [All API references →](https://docs.hanzo.ai/docs/openapi) · [Get started →](https://docs.hanzo.ai/docs/guides)

|                |                                        |
| -------------- | -------------------------------------- |
| **Base URL**   | `https://api.hanzo.ai`                 |
| **Operations** | 26                                     |
| **Auth**       | `Authorization: Bearer $HANZO_API_KEY` |

## Specification

> **HIP-1063 · Auto — Flows That Run Themselves** — Draft · [read the specification →](https://github.com/hanzoai/hips/blob/7ed737180339cd05b5f2935eafe7f7b83d2449a2/HIPs/hip-1063-auto-flows-that-run-themselves.md)

`/v1/auto` is workflows that run themselves, on a schedule or a webhook. An org
authors a flow — a trigger and a tree of connector actions — and this runs it
durably and keeps its run history. It composes three things it does not own:
per-org connector credentials, the one shared durable engine, and the one tenant
gate. It is implemented in `hanzoai/cloud` at `apps/auto` (HIP-0106).

### Motivation

The interesting property of an automation platform is not that it fires; it is
that firing is bounded. A surface that turns one event into runs, and lets a run
emit an event, is a loop generator holding the customer's credentials — so the
budget, the concurrency bound and the causation depth are not operational
knobs bolted on afterwards, they are the specification. The second property is
that a run costs the same once no matter which door started it: a manual start,
a tool call and a schedule tick are three entrances to one recorded run, or they
are three different bills for one thing.

### Specification

The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted
as in RFC 2119.

#### §1 One store, and three things it is not

The store is one SQLite file — the system namespace's `automations`, opened
through the one opener; the sub name is the file key the content encryption
derives from, so it keeps the package's former spelling and a reader should
expect that mismatch rather than read it as drift — holding four tables: flows, their versions, their runs,
and their triggers; the metered claim §5 turns on is a column of the run row. Tenant isolation is the `org` column and it is
physical: every uniqueness and lookup index leads with `org`, so the predicate is
the access path rather than a filter applied to a scan. One connection serializes
writes against the single-writer log.

Three things a reader might expect to find in it are deliberately elsewhere:

- **No credential.** A connector reaches a token only through the integrations
  registry (HIP-1250), which holds them sealed in KMS. This capability never
  touches KMS.
- **No engine.** A run is a durable workflow on the one shared in-process
  durable engine (HIP-1062), started in the owner's namespace on this family's
  own queue. Crash recovery, retry and replay are that engine's, not a second
  copy here.
- **No tool registry.** Every connector action registers into the one tool plane
  (HIP-1213) at mount, and that registration is its only projection: discovery
  is `GET /v1/tools`, dispatch is `POST /v1/tools/call`. There is no second tool
  door.

#### §2 The boundary: what starts it

The neighbouring capability a reader will confuse this with is **flow**
(HIP-1064), and the distinguishing fact is who presses go.

- **flow** is an agent workflow on a visual canvas: a person composes a graph of
  model calls, runs it, and reads the run. It is authored and started by a human
  in a session.
- **auto** is the trigger→action tree that fires with nobody there: a cron
  tick, an inbound provider event, an internal producer. Nobody presses go,
  which is the whole of the distinction.
- **tasks** (HIP-1062) is the durable engine as a product — a customer's own
  workflow, made to survive a crash and replayable. auto is one workflow family
  running on it. Every flow run is a durable run; not every durable run
  is a flow.
- **webhooks** (HIP-1310) is strictly outbound: an app registers an endpoint and
  receives signed deliveries of bus events. `POST /v1/auto/hooks/{source}/{event}`
  is strictly inbound. The two never meet — §6 says nothing here publishes to the
  bus, and nothing on the bus arrives here.
- **integrations** (HIP-1250) holds the org's connected accounts. auto holds
  what to do with them. Connecting an account and using one are separate
  authorities and separate addresses (`/v1/connectors`).

#### §3 The address

`/v1/auto`. Three groups of typed operations, plus a catalogue read:

- `/v1/auto/connectors` — the connector catalogue: each external service a step
  can invoke, its auth descriptor, and the input properties of its actions and
  triggers. It is compiled into the binary and its schema is a build-time
  contract, so a mismatch fails the build rather than the first request. The
  catalogue is identical for every tenant, so the gate is a validated principal
  rather than a per-org view. `/v1/auto/pieces` answers the
  same body under the term the upstream flow contract uses, so a client written
  against that vocabulary reads it without translation; `connector` is the Hanzo
  word (HIP-0126).
- `/v1/auto/flows` — list, create, read, update, delete, plus versions at
  `/v1/auto/flows/{id}/versions`, a start at `/v1/auto/flows/{id}/run`, and
  `enable` / `disable`, where enabling a polling trigger creates its schedule and
  disabling deletes it.
- `/v1/auto/runs` — the run list and one run, refreshed from the engine.

Three routes are declared with prose beside them rather than typed, and each
names a wire fact a typed operation cannot carry. Each is also pinned by a test,
so retyping one turns a test red and names what was lost — prose alone is not a
gate.

1. `POST /v1/auto/flows/{id}/operations` — one route, two response shapes: a
   status change answers with the flow, every other operation with the version
   it edited. An operation declares one `Out`, so typing this would have to
   change one of the two bodies.
2. `POST /v1/auto/runs/{id}/resume` — the payload is an arbitrary JSON value
   delivered verbatim into the paused workflow, while the run is addressed by
   the URL. A struct `In` binds the path and rejects every non-object payload; a
   non-struct `In` accepts them all and receives no path parameter. An `In` whose
   unmarshaller swallows the body is not the third way: it preserves REST and
   silently drops the tool and call-plane projections, where the `In` is the whole
   message and an address that lives only in the URL never arrives.
3. `POST /v1/auto/hooks/{source}/{event}` — the body is an open-keyed event
   payload and the key that selects the flows is in the URL. A struct `In` binds
   the path and drops every key it has no field for, which is a 200, a matched
   trigger, and an empty `{{trigger.*}}` — a silent wrong answer, which is worse
   than a loud refusal.

The last two share one rule, and it is sharper than "an input cannot be both a
body and an address":

\> A typed operation MUST be addressable through its input alone, because on two
\> of its four transports the input is the whole message.

Over the tool plane and the internal call plane the arguments object _is_ the
request and no path is bound from it, so an `In` whose own decoder swallows the
body keeps the REST wire intact and silently makes the operation unaddressable
everywhere else.

#### §4 Tenancy

Every data handler resolves the org from the validated IAM claim (HIP-0026) and
refuses without one. The stored flow's owner is server-derived: a caller cannot
author a flow into another tenant by sending one, because the field a request
might carry is not read.

The durable run carries exactly one credential scope — the owner recorded at
flow start. A step tokenizes against that org and that step's own connector, and
nothing in a step's input can change it. A flow authored by one org can
therefore reach no other org's connection and no other provider's secret.

The inbound path refuses the same way: `Deliver` requires a server-verified org
and starts nothing without one. An unauthenticated producer cannot make a run
happen in anybody's tenant, including their own.

#### §5 Money

The surface declares `cloud.Metered` (`plugin/auto/main.go`).

One unit is metered: a run start, kind `automations.run`, priced from the
deployment's fee (`CLOUD_AUTO_FEE_CENTS`), zero meaning free. The kind, the
durable queue name and the ledger product keep the former spelling on purpose: a
running workflow polls the queue it was started on, and a customer's spend is
read back by the product label already written beside every debit. A tool call
dispatched through the tool plane meters the same unit, because it is the same
work. The debit lands through the org's resource meter on the metered record
(HIP-1313) — the same plane inference bills on, not a private side channel.

The gate runs before the run row is written and before the engine is dispatched,
against the same unit and the same fee the debit uses, so the amount authorized
and the amount charged cannot drift; a deployment that prices a run at zero is
ungated exactly as it is unbilled. It is scoped to the org and not to a project,
because the durable path has no request and therefore no claim-bound project,
and a gate must measure what the debit measures.

Exactly once, whatever started it. The durable path is the single owner of run
bookkeeping: a manual start, a tool call and a schedule tick all record through
it, the run row is idempotent by run id, and the meter fires only for the path
that wins the claim. The metered unit, the observability event of §6 and the
audit record of §6 sit behind that one claim, so the three counts are the same
number.

#### §6 Events and observability

It publishes nothing on the bus: no `auto.*` event reaches a customer's
webhooks. The `/hooks` route is a sink, not a publisher.

Beyond the request span every route already gets, it emits two things, both
under the exactly-once claim of §5: one counter per run on the observability
plane (HIP-1240), tagged with the org and the brand; and one record on the
tamper-evident trail (HIP-1103) per run, per tool call and per mutating HTTP
action, resource type `automations`, carrying the actor, the outcome and the
request id.

#### §7 Bounds

Three ceilings, each on a different amplification, and each MUST hold:

1. **Run starts per rolling minute, per org**, counted from persisted rows so
   the ceiling survives a restart rather than resetting with the process. A
   fan-out or an in-platform loop meets it and stops.
2. **Concurrent run starts and tool executions, per org**, at the front door, so
   one tenant's burst cannot starve another's workers. Per-org and not global:
   a shared counter turns one noisy tenant into everyone's outage.
3. **Causation depth.** An in-platform event carries the number of hops that
   produced it, and one already at the maximum starts nothing. A
   trigger→action→trigger cycle terminates instead of amplifying.

Three sizes bound the store and the wire: a flow tree is capped in step count
and in total serialized bytes at every write, and the resume payload is capped.
An inbound event that carries no idempotency key is given one derived from its
raw body, so a hammer of identical deliveries collapses to one run instead of
minting a fresh run id each time.

#### §8 Stage

`ga`. The manifest row declares no stage, and absent is `ga` (HIP-0139 §8).

#### §9 Upstream

The flow contract is a port, not an embed. The shared trigger and action types
are plain Go structs whose JSON tags match the upstream field names verbatim,
because the flow builder that authors the tree is the contract's consumer and
both halves must read one shape. The connector catalogue served at
`/v1/auto/connectors` is generated from `hanzoai/auto`.

That contract derives from Activepieces Inc.'s open-source automation platform,
under the MIT Expat licence (2020–2024), and `hanzoai/auto` carries the notice.
What survives in HEAD is the MIT-licensed shared contract, ported to Go —
nothing from that upstream's separately-licensed enterprise tree, and no
upstream code runs: the engine here is Go on the shared durable runtime, and a
connector action executes in the in-process runtime HIP-0126 names. Persistence
is `github.com/hanzoai/sqlite`.

### Rationale

The alternative to composing the durable engine is an execution loop inside this
subsystem, which is a second definition of "a run survived a crash" and a second
place to get retry wrong. The alternative to one metered unit at the durable
boundary is metering at each door, which double-counts the moment a door is
added — and a door was added, in the tool plane, at no cost because the meter
was never there.

The alternative to bounding amplification at the plane is bounding it per
connector, which is where the amplification is easiest to see and hardest to
keep correct: every new connector would re-derive the same ceilings, and the one
that forgot would be the incident. Depth, dedupe and the per-org budgets sit at
dispatch, so a connector cannot opt out of them by existing.

### Security Considerations

The wrong implementation lets one org's connected accounts act for somebody
else. The owner recorded at flow start is the only credential scope a step ever
receives, it is the validated claim, and it is neither a field on the flow nor a
field on the event that triggered it. An attacker who can post an event can
start a run and still reach only their own org's tokens.

The second attack is amplification, and it is the one a platform like this
invites: cheap fan-out with the customer's credentials on the far end. The three
ceilings of §7 are the answer, and the run-start ceiling is counted from
persisted rows on purpose — an in-memory counter is reset by the crash the
attacker is causing.

An inbound event body is untrusted and it is threaded into the run as
`{{trigger.*}}`. It is data for a step's input and never a step: the tree that
executes is the published version the org authored, and no payload can add,
reorder or substitute a step. A payload that arrives with keys nobody declared
is carried, not dropped — which is why §3 refuses to type that route, since the
silent version of this is a matched trigger with an empty payload and a 200.

## Four surfaces

| Surface  | Reaches this capability as                   | Coverage                                                                   |
| -------- | -------------------------------------------- | -------------------------------------------------------------------------- |
| **REST** | `auto` at its own prefix                     | 26 operations                                                              |
| **CLI**  | `hanzo auto …`                               | 17 of 26 — the CLI pins the document on its own clock                      |
| **SDK**  | `AutoApi` in every published client          | 26 methods                                                                 |
| **MCP**  | tool `auto` on `https://api.hanzo.ai/v1/mcp` | 14 operations, 1 under the document's own id — ask `describe` for the rest |

## Quickstart

```bash
export HANZO_API_KEY=sk-...   # console.hanzo.ai → API keys
```

Then the first call — a read that needs nothing but the key. `GET /v1/auto/runs`, operation `get_auto_runs`:

**CLI**

```bash
hanzo auto runs list
```

**SDK**

**.ts**

```ts
import { Configuration, AutoApi } from 'hanzoai';

const api = new AutoApi(new Configuration({ accessToken: process.env.HANZO_API_KEY }));
const { data } = await api.getAutoRuns();
```

**.py**

```python
from hanzoai.cloud import ApiClient, Configuration
from hanzoai.cloud.api import AutoApi

client = ApiClient(Configuration(access_token=os.environ["HANZO_API_KEY"]))
result = AutoApi(client).get_auto_runs()
```

**.go**

```go
cfg := hanzoai.NewConfiguration()
cfg.AddDefaultHeader("Authorization", "Bearer "+os.Getenv("HANZO_API_KEY"))
client := hanzoai.NewAPIClient(cfg)

resp, _, err := client.AutoAPI.GetAutoRuns(context.Background()).Execute()
if err != nil {
	return err
}
```

**.rs**

```rust
use hanzo_client::apis::{configuration::Configuration, auto_api};

let mut cfg = Configuration::new();
cfg.bearer_access_token = std::env::var("HANZO_API_KEY").ok();

let result = auto_api::get_auto_runs(&cfg, Default::default()).await?;
```

**.java**

```java
import ai.hanzo.cloud.ApiClient;
import ai.hanzo.cloud.api.AutoApi;

ApiClient client = new ApiClient();
client.setBearerToken(System.getenv("HANZO_API_KEY"));

var result = new AutoApi(client).getAutoRuns();
```

**HTTP**

```bash
curl https://api.hanzo.ai/v1/auto/runs \
  -H "Authorization: Bearer $HANZO_API_KEY"
```

**MCP**

MCP reaches **auto** through the `auto` tool, which names its 14 operations with its own verbs — this one among them, under a name only MCP declares. `describe` explains any of them:

```bash
curl -X POST https://api.hanzo.ai/v1/mcp \
  -H "Content-Type: application/json" \
  -d '{
       "jsonrpc": "2.0",
       "id": 1,
       "method": "tools/call",
       "params": {
         "name": "describe",
         "arguments": {
           "op": "list_auto_flows"
         }
       }
     }'
```

Answers `200` with `object` — ok.

## Endpoints

| Endpoint                                                                                                                                     | What it does                                                                                                                                                                                                       |
| -------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [`POST /v1/auto/automations/{id}/run`](https://docs.hanzo.ai/docs/openapi/auto/post-auto-automations-by-id-run)                              | Starts one run now, whether or not its schedule is armed.                                                                                                                                                          |
| [`POST /v1/auto/automations/{id}/runs/{run}/review`](https://docs.hanzo.ai/docs/openapi/auto/post-auto-automations-by-id-runs-by-run-review) | Answers a run that holds a post for review, for the person the automation runs as or an admin of the org: `post` true posts it to the automation's accounts and answers the run while it posts; false discards it. |
| [`GET /v1/auto/automations/{id}/runs`](https://docs.hanzo.ai/docs/openapi/auto/get-auto-automations-by-id-runs)                              | Returns one automation's runs, newest first, to the person it runs as or an admin of the org.                                                                                                                      |
| [`GET /v1/auto/automations/{id}`](https://docs.hanzo.ai/docs/openapi/auto/get-auto-automations-by-id)                                        | Returns one automation.                                                                                                                                                                                            |
| [`PATCH /v1/auto/automations/{id}`](https://docs.hanzo.ai/docs/openapi/auto/patch-auto-automations-by-id)                                    | Changes an automation, for the person it runs as or an admin of the org: any field it was created with, and `enabled`, which arms or disarms its schedule.                                                         |
| [`DELETE /v1/auto/automations/{id}`](https://docs.hanzo.ai/docs/openapi/auto/delete-auto-automations-by-id)                                  | Deletes an automation, its schedule and its run history, and stops a Dev run it has going.                                                                                                                         |
| [`GET /v1/auto/automations`](https://docs.hanzo.ai/docs/openapi/auto/get-auto-automations)                                                   | Returns the org's automations.                                                                                                                                                                                     |
| [`POST /v1/auto/automations`](https://docs.hanzo.ai/docs/openapi/auto/post-auto-automations)                                                 | Creates an automation and arms its schedule.                                                                                                                                                                       |
| [`POST /v1/auto/flows/{id}/disable`](https://docs.hanzo.ai/docs/openapi/auto/post-auto-flows-by-id-disable)                                  | Disarms a flow's trigger and marks it DISABLED.                                                                                                                                                                    |
| [`POST /v1/auto/flows/{id}/enable`](https://docs.hanzo.ai/docs/openapi/auto/post-auto-flows-by-id-enable)                                    | Arms a flow's trigger and marks it ENABLED.                                                                                                                                                                        |
| [`POST /v1/auto/flows/{id}/operations`](https://docs.hanzo.ai/docs/openapi/auto/post-auto-flows-by-id-operations)                            | Edit a flow — rename it, retarget its trigger, or add, move and delete steps                                                                                                                                       |
| [`POST /v1/auto/flows/{id}/run`](https://docs.hanzo.ai/docs/openapi/auto/post-auto-flows-by-id-run)                                          | Starts one durable run of a flow now.                                                                                                                                                                              |
| [`GET /v1/auto/flows/{id}/versions`](https://docs.hanzo.ai/docs/openapi/auto/get-auto-flows-by-id-versions)                                  | Returns one flow's versions, newest first.                                                                                                                                                                         |
| [`POST /v1/auto/flows/{id}/versions`](https://docs.hanzo.ai/docs/openapi/auto/post-auto-flows-by-id-versions)                                | Adds a new DRAFT version to a flow.                                                                                                                                                                                |
| [`GET /v1/auto/flows/{id}`](https://docs.hanzo.ai/docs/openapi/auto/get-auto-flows-by-id)                                                    | Returns one automation and its latest version.                                                                                                                                                                     |
| [`PATCH /v1/auto/flows/{id}`](https://docs.hanzo.ai/docs/openapi/auto/patch-auto-flows-by-id)                                                | Updates one automation's metadata in place.                                                                                                                                                                        |
| [`DELETE /v1/auto/flows/{id}`](https://docs.hanzo.ai/docs/openapi/auto/delete-auto-flows-by-id)                                              | Deletes one automation, its versions and its run history.                                                                                                                                                          |
| [`GET /v1/auto/flows`](https://docs.hanzo.ai/docs/openapi/auto/get-auto-flows)                                                               | Returns the caller org's automations, most-recently-updated first.                                                                                                                                                 |
| [`POST /v1/auto/flows`](https://docs.hanzo.ai/docs/openapi/auto/post-auto-flows)                                                             | Creates an automation and its initial DRAFT version in one call.                                                                                                                                                   |
| [`POST /v1/auto/hooks/{source}/{event}`](https://docs.hanzo.ai/docs/openapi/auto/post-auto-hooks-by-source-by-event)                         | Fire an event that starts every enabled flow subscribed to it                                                                                                                                                      |
| [`POST /v1/auto/provider/{id}/run`](https://docs.hanzo.ai/docs/openapi/auto/post-auto-provider-by-id-run)                                    | Executes one provider action in-process and answers the outcome.                                                                                                                                                   |
| [`GET /v1/auto/provider`](https://docs.hanzo.ai/docs/openapi/auto/get-auto-provider)                                                         | Returns the connector catalogue.                                                                                                                                                                                   |
| [`POST /v1/auto/runs/{id}/resume`](https://docs.hanzo.ai/docs/openapi/auto/post-auto-runs-by-id-resume)                                      | Release a run waiting at an approval step, with the approval payload                                                                                                                                               |
| [`GET /v1/auto/runs/{id}`](https://docs.hanzo.ai/docs/openapi/auto/get-auto-runs-by-id)                                                      | Returns one run.                                                                                                                                                                                                   |
| [`GET /v1/auto/runs`](https://docs.hanzo.ai/docs/openapi/auto/get-auto-runs)                                                                 | Returns the caller org's run history, newest first.                                                                                                                                                                |
| [`GET /v1/auto/templates`](https://docs.hanzo.ai/docs/openapi/auto/get-auto-templates)                                                       | Returns the starter automations: a name, what it does, the instructions it runs and the schedule it suggests, in the caller's own zone.                                                                            |

---

[All Hanzo APIs](https://docs.hanzo.ai/docs/openapi) · [Interactive reference](https://docs.hanzo.ai/docs/openapi/reference)
