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.

KeyTypeRequiredDescriptionProcess manifest
idstringyesAdapter id, such as 'attio/assert'. Named by the contract owner (ADR-026). The id's vendor prefix is the COST provider name.same key
versionintegeryesProcess manifest: Manifest version; part of the provenance string.same key
roleone of source, traverse, enrich, deliveryessource: 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_typestringyesA 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
fromstringnoTraverse only (ADR-054): the input type; the planner requires it to equal the current segment's type.same key
relationobjectnoTraverse 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.namestring matching ^[a-z][a-z0-9_]*$yes—same key
relation.fromone of record, parentyes—same key
needsanynoSame manifest surface as process adapters (SPEC.md §6): a JSON Schema, 'dynamic', or the dynamic-with-floor object form.same key
providesanynoSame manifest surface as process adapters (SPEC.md §6).same key
config_schemaobjectnoProcess manifest: JSON Schema for the step's with: block.same key
freshness_daysintegernoProcess manifest: Default cache window for the fields this adapter provides; overridable per step with cache: (SPEC.md §6, §9).same key
cost_estimate_usdnumbernoProcess manifest: Static per-record cost estimate. gtme plan prints '?' when absent (SPEC.md §7).same key
keep_payloadsbooleannoADR-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_daysintegernoADR-030: retention window for this binding's payloads (default 90). 0 = no expiry.same key
credentialsarray of stringnoProcess manifest: Env var names the runner MUST inject. A missing one is a plan-time error (SPEC.md §6).same key
credentials_optionalarray of stringnoProcess 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
idempotencyone of native, ledgernoWhich 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_scopestringnoDeliver 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

KeyTypeDescription
emits_key_fieldsarray of stringSources only: which output fields the runner should build the identity key from (SPEC.md §6).
batchbooleanMarks adapters the runner must feed in batches — the AI steps — one invocation per batch_size records (SPEC.md §6, §9).
attestsbooleanADR-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).
preflightsbooleanADR-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.

KeyTypeRequiredDescription
auth.typeone of header, query, basic, beareryes—
auth.namestringnoHeader or query-param name; unused for basic/bearer.
auth.envstringyesEnv var holding the credential (resolved per SPEC.md §6 credentials rules).
auth.prefixstringnoOptional 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.

KeyTypeRequiredDescription
request.methodone of GET, POST, PUT, PATCH, DELETEyes—
request.urlstringyes—
request.querymap of stringnoQuery-param templates; empty resolutions are omitted.
request.headersmap of stringno—
request.bodyanynoBody 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.

KeyTypeRequiredDescription
pagination.strategyone of page, cursor, offsetyes—
pagination.paramstringnoRequest param carrying the page number / cursor / offset.
pagination.inone of query, bodynoWhere pagination params ride (default query).
pagination.cursor_pathstringnoDotted path to the next cursor in the response (cursor strategy).
pagination.page_sizeinteger or stringnoRecords per page: an integer or a template (such as '{{config.per_page}}').
pagination.size_paramstringnoRequest param carrying the page size.
pagination.terminationobjectno—
pagination.termination.empty_recordsbooleannoStop when a page yields zero records (engine default: always on).
pagination.termination.short_pagebooleannoStop when a page yields fewer records than page_size.
pagination.termination.total_pages_pathstringnoDotted path to a total-page count; alternatives via '|'.
pagination.maxintegernoHard cap on pages fetched.

Extract

The extract block, optional.

KeyTypeRequiredDescription
extract.recordsstring or array of stringyesDotted path to the record array (or single record) in the response; a list means first non-empty wins.
extract.fieldsobjectyescanonical (or vendor-namespaced) field name -> extraction rule.
extract.fields.FIELDstring or objectno—
extract.fields.FIELD.pathstringno—
extract.fields.FIELD.pathsarray of stringnoAlternatives; first non-empty wins.
extract.fields.FIELD.transformstringnoA 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.absentarray of anynoSentinel 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_inputbooleannoOmit 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.eachstringnoThe 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.templatestringnoThe 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.limitinteger or stringnoThe 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.

KeyTypeRequiredDescription
errors.STATUSobject or array of objectno—
errors.STATUS.verdictone of fail_record, fail_run, retry, skipyes—
errors.STATUS.reasonstringno—
errors.STATUS.matchstringnoA 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.classone of auth, rate_limit, network, providernoThe 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[].verdictone of fail_record, fail_run, retry, skipyes—
errors.STATUS[].reasonstringno—
errors.STATUS[].matchstringnoA 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[].classone of auth, rate_limit, network, providernoThe 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.

KeyTypeRequiredDescription
cost.perone of record, request, unitno—
cost.amount_usdnumber or string matching ^\s*\{\{[^{}]+\}\}\s*$noDollars 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_pathstringnoDotted path to a units-consumed count in the response (per: unit).

Retry

The retry block, optional.

KeyTypeRequiredDescription
retry.max_attemptsintegerno—
retry.backoff_secondsnumberno—
retry.rate_per_hourintegerno—
retry.windowsarray of stringnoHourly 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.

KeyTypeRequiredDescription
session.paramstringno—
session.inone of header, query, bodyno—

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