Binding manifest
Every key of a binding.yaml, block by block, and which keys it shares with a process adapter's manifest, generated from spec/binding-schema.json
What you'll learn
You're writing a binding for a vendor gtme doesn't ship and need the exact key, its type, and what the engine does with it.
- every key a binding accepts, block by block
- which keys a binding shares with a process adapter's manifest
- a complete binding to start from
Every key spec/binding-schema.json allows in a binding.yaml, block by block, with the keys it shares with a process adapter's manifest.json (spec/schemas/manifest.schema.json). A dotted key sits inside the key before the dot, and an upper-case part such as FIELD stands for a name you choose.
A tier-1 adapter: a YAML document the runner's generic HTTP engine interprets deterministically (SPEC.md §10a, ADR-022). Validate after decoding the YAML to JSON. Deliberately ~8 primitives; the moment a binding needs logic it graduates to a process adapter — no expression language may ever grow in here. Templating (ADR-057, M31): a leaf is a Liquid object in gtme's one bounded dialect over 'record.<field>', 'config.<key>', 'variables.<name>' and 'session' — objects only, no block tags, the fixed filter allowlist (SPEC.md §10 item 10; 'default' is the fallback: '{{ record.linkedin_url | default: record.linkedin_internal_url }}'). A leaf that is exactly one placeholder substitutes the typed value; leaves resolving to empty/null are omitted from the request (omitempty). The retired '{{a|b}}' alternatives fail verify naming the rewrite. In a deliver body, an object may carry the key '$variables': true — the resolved variables: mapping (minus any variable referenced individually elsewhere in the body) is spliced into that object. The engine pre-fills config defaults declared in config_schema.
Manifest surface
The keys that say what the binding is. Process manifest says whether a process adapter's manifest has the same key; where the binding schema gives no description, the process manifest's is shown.
| Key | Type | Required | Description | Process manifest |
|---|---|---|---|---|
id | string | yes | Adapter id, such as 'attio/assert'. Named by the contract owner (ADR-026). The id's vendor prefix is the COST provider name. | same key |
version | integer | yes | Process manifest: Manifest version; part of the provenance string. | same key |
role | one of source, traverse, enrich, deliver | yes | source: pagination + cursor/STATE, config 'limit' caps emitted records; traverse (ADR-054): a source-shaped request templated per parent from '{{record.<field>}}' placeholders (its needs), emitting records of entity_type related to the parent by 'relation', 'limit' capping children per parent; enrich: per-record request; deliver: idempotency + dry-run receipts. | same key |
entity_type | string | yes | A type file's name (SPEC.md §4a). For a traverse this is the OUTPUT type; 'from' is the input. A binding MAY ship the type it emits as types/<name>.json beside binding.yaml (never an embedded name). | same key |
from | string | no | Traverse only (ADR-054): the input type; the planner requires it to equal the current segment's type. | same key |
relation | object | no | Traverse only (ADR-054): the edge written between every emitted record and its parent, and which end it starts at (record: authored_by from the post to its author; parent: works_at from the person to the company). | same key |
relation.name | string matching ^[a-z][a-z0-9_]*$ | yes | — | same key |
relation.from | one of record, parent | yes | — | same key |
needs | any | no | Same manifest surface as process adapters (SPEC.md §6): a JSON Schema, 'dynamic', or the dynamic-with-floor object form. | same key |
provides | any | no | Same manifest surface as process adapters (SPEC.md §6). | same key |
config_schema | object | no | Process manifest: JSON Schema for the step's with: block. | same key |
freshness_days | integer | no | Process manifest: Default cache window for the fields this adapter provides; overridable per step with cache: (SPEC.md §6, §9). | same key |
cost_estimate_usd | number | no | Process manifest: Static per-record cost estimate. gtme plan prints '?' when absent (SPEC.md §7). | same key |
keep_payloads | boolean | no | ADR-030: whether the engine attaches raw responses to this binding's RECORDs for retention. Default true; steps may override with keep_payloads: false in config. | same key |
payload_ttl_days | integer | no | ADR-030: retention window for this binding's payloads (default 90). 0 = no expiry. | same key |
credentials | array of string | no | Process manifest: Env var names the runner MUST inject. A missing one is a plan-time error (SPEC.md §6). | same key |
credentials_optional | array of string | no | Process manifest: Injected when present, never blocks a plan (SPEC.md §6). Used by adapters that can work several ways, such as an AI step with a local engine. | same key |
idempotency | one of native, ledger | no | Which party guarantees delivery dedupe: the vendor's own semantics (native, such as an assert/upsert endpoint) or the runner's deliveries table (ledger). Required for role: deliver. | same key |
idempotency_scope | string | no | Deliver bindings (ADR-044): config key whose resolved value scopes deliveries rows (SPEC §8). Optional. The key MUST hold a stable identifier of the destination (an id, an API slug, a file path, a URL), never a display name its owner can change (ADR-062); where the identifier has a shape, constrain the key to it in config_schema so a name is a plan error. | same key |
Process manifest keys a binding does not take
| Key | Type | Description |
|---|---|---|
emits_key_fields | array of string | Sources only: which output fields the runner should build the identity key from (SPEC.md §6). |
batch | boolean | Marks adapters the runner must feed in batches — the AI steps — one invocation per batch_size records (SPEC.md §6, §9). |
attests | boolean | ADR-036, deliver adapters only: after a successful create the adapter re-reads the target and emits an ATTEST per record (SPEC.md §5) — confirmed | contradicted | inconclusive. Absent, every delivery stays accepted and is reported inconclusive (the honest default). |
preflights | boolean | ADR-040, deliver adapters only: the adapter can check the live target against what a step is about to send (SPEC.md §5 PREFLIGHT) — read-only, at --dry-run and arm time. Absent, no preflight runs. |
Auth
The auth block, optional.
| Key | Type | Required | Description |
|---|---|---|---|
auth.type | one of header, query, basic, bearer | yes | — |
auth.name | string | no | Header or query-param name; unused for basic/bearer. |
auth.env | string | yes | Env var holding the credential (resolved per SPEC.md §6 credentials rules). |
auth.prefix | string | no | Optional literal prefix for the value, such as 'Bearer '. |
Request
The request block, required. Templating draws only from step config, the record's canonical fields, resolved variables, and the session id — no expressions, no computation.
| Key | Type | Required | Description |
|---|---|---|---|
request.method | one of GET, POST, PUT, PATCH, DELETE | yes | — |
request.url | string | yes | — |
request.query | map of string | no | Query-param templates; empty resolutions are omitted. |
request.headers | map of string | no | — |
request.body | any | no | Body template: a JSON shape whose string leaves are templates. Empty resolutions are omitted. '$variables': true in an object splices the resolved variables map. |
Pagination
The pagination block, optional.
| Key | Type | Required | Description |
|---|---|---|---|
pagination.strategy | one of page, cursor, offset | yes | — |
pagination.param | string | no | Request param carrying the page number / cursor / offset. |
pagination.in | one of query, body | no | Where pagination params ride (default query). |
pagination.cursor_path | string | no | Dotted path to the next cursor in the response (cursor strategy). |
pagination.page_size | integer or string | no | Records per page: an integer or a template (such as '{{config.per_page}}'). |
pagination.size_param | string | no | Request param carrying the page size. |
pagination.termination | object | no | — |
pagination.termination.empty_records | boolean | no | Stop when a page yields zero records (engine default: always on). |
pagination.termination.short_page | boolean | no | Stop when a page yields fewer records than page_size. |
pagination.termination.total_pages_path | string | no | Dotted path to a total-page count; alternatives via '|'. |
pagination.max | integer | no | Hard cap on pages fetched. |
Extract
The extract block, optional.
| Key | Type | Required | Description |
|---|---|---|---|
extract.records | string or array of string | yes | Dotted path to the record array (or single record) in the response; a list means first non-empty wins. |
extract.fields | object | yes | canonical (or vendor-namespaced) field name -> extraction rule. |
extract.fields.FIELD | string or object | no | — |
extract.fields.FIELD.path | string | no | — |
extract.fields.FIELD.paths | array of string | no | Alternatives; first non-empty wins. |
extract.fields.FIELD.transform | string | no | A registry normalization rule id (SPEC.md §4a) — or 'linkedin', the engine-owned §4 classify-and-route rule that stores a LinkedIn-URL-shaped value under the field matching its shape. Never arbitrary logic. |
extract.fields.FIELD.absent | array of any | no | Sentinel values the vendor uses to mean 'no value' (such as a locked-email placeholder, or 0 for an unknown count); matched exactly, treated as absent. |
extract.fields.FIELD.skip_if_input | boolean | no | Omit this output field when the projected input already carried a non-empty value for it (for example, only emit a resolved public linkedin_url when the record arrived without one). |
extract.fields.FIELD.each | string | no | The list form (SPEC.md §10a, ADR-059): a dotted path to an array ('|' alternatives allowed, first non-empty wins). The template renders once per element over item.*, a render that is empty after trimming is dropped, rendering stops at limit, and the field provides an array of strings. Does not mix with path, paths or transform. |
extract.fields.FIELD.template | string | no | The each: template, in the ADR-057 dialect over item.* — tags allowed, since it renders text, not a typed value. Only with each. |
extract.fields.FIELD.limit | integer or string | no | The most renders each: keeps: a positive integer, or a config reference ({{ config.<key> }}). Only with each; omitted means every element. |
Errors
The errors block, optional. HTTP status (or status class like '4xx') -> one rule, or an ordered list of rules (SPEC.md §10a Error verdicts, ADR-065). The first rule whose match occurs in the response body, or that has no match, applies; a rule after one with no match can never apply and is refused. A status with no applicable rule follows the engine's default classification (SPEC.md §8 exit-code classes). fail_run stops the step; fail_record and retry fail the record; skip counts it skipped.
| Key | Type | Required | Description |
|---|---|---|---|
errors.STATUS | object or array of object | no | — |
errors.STATUS.verdict | one of fail_record, fail_run, retry, skip | yes | — |
errors.STATUS.reason | string | no | — |
errors.STATUS.match | string | no | A literal substring of the raw response body: no pattern syntax, no path, no operator. The rule applies only when the body contains it. A rule that applied because of its match and names no class reclassifies the error as provider (exit 1). |
errors.STATUS.class | one of auth, rate_limit, network, provider | no | The error's class (SPEC.md §8 exit codes 3, 4, 5, 1). class: auth says the body means a bad credential: the run exits 3 and the step stops as an auth failure does. |
errors.STATUS[].verdict | one of fail_record, fail_run, retry, skip | yes | — |
errors.STATUS[].reason | string | no | — |
errors.STATUS[].match | string | no | A literal substring of the raw response body: no pattern syntax, no path, no operator. The rule applies only when the body contains it. A rule that applied because of its match and names no class reclassifies the error as provider (exit 1). |
errors.STATUS[].class | one of auth, rate_limit, network, provider | no | The error's class (SPEC.md §8 exit codes 3, 4, 5, 1). class: auth says the body means a bad credential: the run exits 3 and the step stops as an auth failure does. |
Cost
The cost block, optional.
| Key | Type | Required | Description |
|---|---|---|---|
cost.per | one of record, request, unit | no | — |
cost.amount_usd | number or string matching ^\s*\{\{[^{}]+\}\}\s*$ | no | Dollars per unit of per: a number, or a template resolved from config (such as '{{config.cost_per_record_usd}}') for vendors whose price is plan-dependent (ADR-046). Declare the knob in config_schema. An unresolved template costs $0 at run time and gtme plan prints est/record: unset. A page-billed endpoint declares per: request — per: record counts emitted records, and a limit truncates emission after the vendor has billed the page. |
cost.unit_path | string | no | Dotted path to a units-consumed count in the response (per: unit). |
Retry
The retry block, optional.
| Key | Type | Required | Description |
|---|---|---|---|
retry.max_attempts | integer | no | — |
retry.backoff_seconds | number | no | — |
retry.rate_per_hour | integer | no | — |
retry.windows | array of string | no | Hourly send/fetch windows, such as '09:00-17:00'. |
Session
The session block, optional. Optional pagination-consistency session: a UUID minted per run and passed through on every request.
| Key | Type | Required | Description |
|---|---|---|---|
session.param | string | no | — |
session.in | one of header, query, body | no | — |
Example
From spec/bindings/apollo-search/binding.yaml:
# apollo/search — THE apollo/search adapter (built into gtme until ADR-059),
# rewritten for the masked API surface (ADR-043). Apollo withdrew value
# fields from API search 2026-08-30: `mixed_people/search` 422s for API
# callers, and its replacement `mixed_people/api_search` returns masked rows
# (no email, no linkedin, `has_*` booleans, obfuscated last name). This
# binding is honest about that: it provides the masked surface at $0, and
# the revealed person is apollo/enrich's job — the canonical composition
# filters on these free fields first and reveals only past the filter.
#
# Identity (§4): a masked row carries no email, no linkedin and no company
# domain, so identity derives from the name-hash tier — which needs a first
# AND last name. `last_name` therefore carries Apollo's own obfuscated form
# (e.g. "D.") rather than being dropped: it keys the record, `gtme plan`
# notes the weak tier, and apollo/enrich supersedes it with the real value
# on reveal. Distinct people who share a first name and an obfuscated
# initial in one pull would collide on this tier — reveal early if that
# matters.
#
# Endpoint: POST https://api.apollo.io/api/v1/mixed_people/api_search
# Auth: X-Api-Key header
# Docs: https://docs.apollo.io/reference/people-api-search
id: apollo/search
version: 2
role: source
entity_type: person
provides:
type: object
additionalProperties: false
properties:
first_name: { type: string }
last_name: { type: string }
title: { type: string }
company_name: { type: string }
apollo.id: { type: string }
apollo.has_email: { type: boolean }
config_schema:
type: object
additionalProperties: false
anyOf:
- required: [query]
- required: [titles]
- required: [seniorities]
- required: [locations]
- required: [domains]
properties:
query:
type: string
description: Free-text search, such as "vp marketing, saas, 50-200 employees"
limit:
type: integer
minimum: 1
description: Stop after this many people
titles:
type: array
items: { type: string }
description: person_titles
seniorities:
type: array
items: { type: string }
description: person_seniorities
locations:
type: array
items: { type: string }
description: organization_locations
employee_ranges:
type: array
items: { type: string }
description: organization_num_employees_ranges, such as "50,200"
domains:
type: array
items: { type: string }
description: q_organization_domains
per_page:
type: integer
minimum: 1
maximum: 100
default: 25
base_url:
type: string
default: "https://api.apollo.io"
description: Override the API host (testing)
credentials: [APOLLO_API_KEY]
cost_estimate_usd: 0
auth:
type: header
name: X-Api-Key
env: APOLLO_API_KEY
request:
method: POST
url: "{{config.base_url}}/api/v1/mixed_people/api_search"
headers:
Cache-Control: no-cache
body:
q_keywords: "{{config.query}}"
person_titles: "{{config.titles}}"
person_seniorities: "{{config.seniorities}}"
organization_locations: "{{config.locations}}"
organization_num_employees_ranges: "{{config.employee_ranges}}"
q_organization_domains_list: "{{config.domains}}"
pagination:
strategy: page
param: page
in: body
size_param: per_page
page_size: "{{config.per_page}}"
termination:
empty_records: true
short_page: true
extract:
records: people
fields:
apollo.id: id
first_name: first_name
last_name: last_name_obfuscated
title: title
company_name: organization.name
apollo.has_email: has_email
cost:
per: record
amount_usd: 0
See also
Used in
- Add a vendor: Every key a binding.yaml can declare, including the transform rules
- Two adapter tiers: Every key a binding.yaml can declare