Skip to main content
Every payload you send to Brew has a contract: the trigger’s payloadSchema, declared when the trigger is created (integration triggers declare theirs per provider event). Every fire of POST /v1/automations/triggers/{triggerEventId}/fire is validated against it. You can also store a richer contract per trigger, versioned and renderable as types, which the /contract endpoints below cover. This guide turns those contracts into TypeScript types in your codebase, keeps them from drifting, and shows where Brew checks real fires against them.
Nested payload values (objects and arrays) are accepted everywhere. Address a nested leaf by its dotted path: {{ user.plan }}, not {{ payload.user.plan }}. See Merge tags and variables.

Generate Types with brew-cli

brew-cli types writes one file with a type per trigger contract (the command needs a key with the automations scope):
Every trigger in the workspace is included (every page of the workspace list, nothing is truncated), transactional-purpose flows and marketing flows alike. The contract lives on the trigger either way. The output is deterministic, with a content hash in the header:
Type names follow one rule everywhere (the in-app Copy as TypeScript, the SKILL.md brief, and this file): PascalCase of the trigger title, plus Payload. Renaming a trigger renames the type, which is exactly the drift --check exists to catch; colliding names get the trigger id appended so the file always compiles. Field optionality follows the schema: a trigger field is optional when required: false. The email field is always required. It is the contact key downstream automations route on. In CI, gate drift with:
The command exits 1 when the workspace’s contracts no longer match the committed file, which is the signal to regenerate and review the diff. A trigger schema edit shows up as a type change in code review instead of a runtime surprise.

Store a Contract

Brew derives a contract on every read, so the guide above works with no setup. You can also store one per trigger. A stored contract is content-hash versioned and served from GET /v1/automations/triggers/{triggerEventId}/contract. The response says which kind you got. source: "stored" carries a contractHash, a version, and an enforcement mode. source: "derived_from_schema" is the fallback, built from the trigger’s flat payloadSchema. It can carry type: "unknown" where there was no type evidence to work from. Fields form a tree. Scalars are string, int, float, boolean, date, or enum. An object node carries children. An array node carries either children for an element object shape, or itemType for scalar elements. Pass ?format= to get a generated artifact instead of the JSON contract:
ts, zod, jsonschema, and skill each return { format, content }. Declare or replace the stored contract with PUT on the same URL:
Four rules are worth knowing before you write one:
  • The whole tree validates before anything is written. Unknown-typed nodes are refused, so declare a concrete type. Keys must be unique per level, and an object field must declare at least one child.
  • A trigger contract keeps its email field. It needs a top-level { "key": "email", "type": "string", "required": true } so automations can resolve a recipient.
  • fields is optional. Changing only the enforcement mode is a PUT with just enforcement, so you never re-send the tree to flip a switch.
  • The version tracks behavior, not prose. Keys, types, required, and fallbacks bump it. Editing a description or an example does not.
Declaring a contract does not change what happens at fire time. Enforcement is off until you turn it on, and the two enforcing modes are prune and strict. They differ only in how they treat keys the contract does not declare. prune drops them, strict fails the payload. Declared-field rules are identical in both.

Draft a Contract from an Example

Writing a field tree by hand is tedious when you already have the payload. Post a real example to POST /v1/payload-contracts/infer and Brew types it for you:
Integers and floats split automatically, full ISO timestamps become date, and object and array shapes are walked recursively. Every inferred field comes back required: true, so relax the optional ones yourself. Inference degrades honestly. A null or an empty array carries no type evidence, so those land in issues with the offending path instead of a guess. Nothing is saved either way: review the draft, then PUT it to the trigger’s /contract.

Dry-Run a Payload

POST /v1/automations/triggers/{triggerEventId}/contract/validate runs a payload through the same validator the live path uses, against the stored contract when one exists. Nothing fires, sends, or writes.
An invalid payload still returns 200. The verdict is the body, not the status code, so check valid rather than the response status.
Read valid, the per-field errors and warnings, resolvedPayload with fallbacks applied, and prunedKeys for anything the contract does not declare. The optional enforcement field previews a mode other than the stored one. That is how you find out what strict would reject before you arm it.

Pin the Types in SDK Calls

fire accepts a payload type parameter, so call sites compile against the contract:
A missing required field or a wrong scalar type is a compile error. The wire request is unchanged; the generics are type-level only.

Fetch the Wiring Brief for an Agent

The trigger read accepts ?include=skill, which adds a skill field: a complete SKILL.md-shaped brief (endpoint, auth, the typed contract inline, copy-paste snippets, and a test loop) that a coding agent can follow to wire your service.
Save the field as SKILL.md in your repo, or hand the URL to an agent. The in-app contract panel offers the same file under Copy as → Download SKILL.md, and it is generated from the same source as the API response.

Preflight Before the First Fire

GET on the fire endpoint verifies the exact credential you will use, without firing:
A 200 with status: "ready" means the key, brand scope, and permissions all pass, and details carries the payload contract plus what a fire would start. counts.automations: 0 means fires are accepted and logged but start no runs until an automation wired to the trigger is published.

Verify Live Fires in the App

Brew checks what your service actually sent against the contract:
  • A trigger fire’s detail sheet (Events page) shows a Contract check: the raw body replayed through the same validator the live fire used, so a key the fire dropped is labeled exactly as dropped.
  • The trigger’s page walks you through wiring with a step list whose completion is derived from real data: a key exists, your test fire arrived, an automation is live.
Together the loop is: generate types, pin them in calls, gate drift in CI, and read the live ledger when something looks off.