Building Pipelines
Reading and writing data
Read from and write to Connectors with call_method, stream pages with backpressure, and batch writes safely.
Reading and writing data
Almost all I/O in a task goes through the call_method step: it calls a method on one of the task's Connectors and feeds the result into the pipeline. This page shows how to read a collection with streaming backpressure, write each record to a destination, and control batching.
Prerequisites
- A task file and the authoring loop from Task anatomy and authoring workflow
- Connector configurations for your source and destination types
The call_method step
call_method takes three config keys:
| Key | Required | Description |
|---|---|---|
connector |
Yes | Connector slot name from the task's connectors block |
method |
Yes | Method name — depends on the connector type |
args |
No | Arguments passed to the connector method |
See call_method for the full key table and behavior notes.
A source step usually needs no reference to pipeline input, so its config is a static object:
- name: read_users
type: call_method
config:
connector: source
method: read
args:
resource: usersA write step references the current record, so its config is a JSONata string:
- name: upsert_user
type: call_method
config: |
{
"connector": "destination",
"method": "upsert",
"args": {
"table": "users",
"data": input,
"conflict_columns": ["id"]
}
}How collections stream
A step's return value determines what is emitted downstream:
| Return value | Behavior |
|---|---|
undefined |
Item is dropped — nothing emitted |
[] (empty array) |
Item is dropped — nothing emitted |
| Array | Auto-unwrapped — each element is emitted as a separate item |
| Anything else | Emitted as a single item |
Connector methods that return collections (find, read, list, proxy_list) fan out: each record is emitted downstream as its own item, as it arrives. Downstream steps apply backpressure — when a slow write step is at its concurrency limit, upstream reads pause — so the full result set is never held in memory.
Two earlier behaviors are not controlled by the return value: when a step's config evaluates to null/undefined, or when run_if/skip_if skip the step, the original input passes through unchanged.
A complete task reading from one Connector and writing to another:
name: csv-to-postgres
description: Read users from CSV and upsert them into PostgreSQL
output: none
connectors:
source:
type: csv
destination:
type: postgres
steps:
- name: read_users
type: call_method
config:
connector: source
method: read
args:
resource: users
- name: upsert_user
type: call_method
concurrency: 5
config: |
{
"connector": "destination",
"method": "upsert",
"args": {
"table": "users",
"data": input,
"conflict_columns": ["id"]
}
}read_users emits one item per CSV row. Each row flows to upsert_user independently; concurrency: 5 lets five upserts run in parallel while backpressure keeps the reader in step. output: none suppresses stdout because the destination write is the point of the task.
Note that a call_method step's return value replaces input for the next step — after upsert_user, input is the upsert result, not the original row. If a later step needs a field from the original record, carry it forward in a prior transform and read it via steps.{name}.output.
Pagination
Connectors handle pagination internally. API connectors' collection methods stream page by page and keep fetching until the source is exhausted — for example, the Jira and Freshdesk find methods auto-paginate, and catalog connectors' proxy_list streams all pages by default. Database connectors stream driver cursors the same way (MongoDB find and aggregate iterate the cursor lazily). Your task does not implement paging loops; it consumes one already-streaming source.
Two related task keys exist for cursor persistence across runs: the top-level cursor_store key names a connector slot used to store cursors, and a step may carry a cursor key with a key field. Most read/write tasks do not need either.
For one-task-per-resource design — why a pipeline has exactly one streaming source, and how to combine several pulls in a Job — see the guidance in Task anatomy and authoring workflow.
Choosing a connector method
Each connector type exposes its own method set (find/upsert on SQL databases, read on CSV, list/create on API connectors). The per-connector pages list every method with its args and return type — see the Connectors overview and, for example, PostgreSQL. The validator checks the method name against the connector type's schema and rejects unknown methods at validation time.
Batching writes
Two step keys control how output is grouped before moving downstream:
batchcollects a step's output items into arrays of N (the final batch may be smaller). Use it when the destination accepts bulk payloads.spoolcollects all output into a single array, emitted once when the stream ends. Use it only for aggregation — it is unbounded by design.
- name: read_users
type: call_method
batch: 100
config:
connector: source
method: read
args:
resource: usersVerify it worked
Run the task with --debug and watch each record flow through on stderr:
envoy csv-to-postgres --config connectors.yaml --debugThen query the destination (in Data Explorer or directly) and confirm the row count matches the source.
Related
- call_method — full configuration reference
- Connectors overview — per-connector method tables
- Transforming and filtering — shape records between read and write
- Idempotent sync and the mapping DB — make writes safe to re-run