Siphondocs
Connector Spec

Enrich

Fan out dependent calls as a DAG — per record or batched — with caching and per-call error policy.

Most list endpoints return thin records. enrich fans out dependent calls to fill them in — a detail lookup, a related collection, a second-hop join — forming a DAG that the engine runs level by level. The results are attached to each record under assignTo and become available to the transform.

enrich:
  - id: detail
    dependsOn: [list]                     # the fetch id, or another enrich id
    method: GET
    path: /api/Deals/{{ record.id }}
    cardinality: one                      # one | many
    extract: { dataPath: "$" }
    assignTo: _detail
    onError: continue
  - id: client
    dependsOn: [detail]                   # runs after detail attaches
    when: "record._detail.clientId"       # skip when falsy
    path: /api/Clients/{{ record._detail.clientId }}
    extract: { dataPath: "$" }
    assignTo: _client
    onError: continue

Fields

FieldPurpose
idUnique call id; referenced by dependsOn.
dependsOnOne or more upstream ids (fetch or enrich). Defines DAG order.
whenOptional template predicate — skip the call when it evaluates falsy.
method / path / query / headers / bodyThe request (same shape as fetch).
cardinalityone (attach an object) or many (attach an array).
batchTurn N per-record calls into batched calls (see below).
cacheMemoize identical calls within a scope.
extract.dataPathJSONPath into the response to attach.
assignToThe record key to attach under (e.g. _detail).
onErrorcontinue (default) · fail_record · fail_run.

The DAG is validated acyclic at publish time, and template values resolve across hops — {{ record._detail.clientId }} works because detail attaches before client runs.

Two-level joins

Because a later call can depend on data an earlier call attached, you can express a join the list DTO can't give you directly — fetch the detail (which carries a clientId), then fetch the client:

list ──▶ detail ──▶ client

A when guard (record._detail.clientId) skips the second hop for records that have no client, so you don't waste calls on 404s.

Batching

When a source offers a bulk endpoint, batch collapses N per-record calls into ceil(N / chunkSize) calls. The chunk of keys is exposed as {{ batchKeys }}, and results are matched back to records by matchPath.

- id: contacts
  dependsOn: [list]
  path: /contacts
  query: { ids: "{{ batchKeys }}" }
  batch:
    keyPath: "$.contactId"   # which record field to collect (default $.id)
    matchPath: "$.id"        # which response field maps back
    chunkSize: 100           # max 2000
  cardinality: one
  extract: { dataPath: "$" }
  assignTo: _contact

Caching

cache memoizes identical calls so a shared lookup (say, a project referenced by many deals) is fetched once.

cache:
  key: "project:{{ record.projectId }}"
  scope: run        # run | connection | global (not org)
  ttlSeconds: 3600

Error policy

onError decides what a failed enrich call does:

ValueEffect
continue (default)Log the error, leave the field unset, keep going.
fail_recordDrop just this record from the run.
fail_runAbort the whole run.

continue is the default on purpose: a flaky detail endpoint should degrade a few records, not fail an entire sync. The Run Inspector still surfaces every enrich error.

On this page