Profile
Companion to the Personal Data Portability Protocol (PDPP) core spec.
Date: 2026-04-11
Scope: How data is collected into a PDPP resource server: connector runtime, JSONL protocol, bindings, and sync state.
Companion to the Personal Data Portability Protocol (PDPP) core spec.
Overview
The Collection Profile defines how connectors collect data from source platforms and write it to a PDPP resource server. It is one fulfillment mechanism for the PDPP core protocol; pre-collected data, manual imports, and other ingestion mechanisms are equally valid.
The Collection Profile is architecturally separate from the core protocol. A resource server serving pre-collected data needs no awareness of this profile. A connector runtime implementing this profile needs no awareness of grant semantics beyond what is explicitly passed to it in the START message.
Collection method abstraction
Connectors abstract over the source platform's data access interface. The runtime does not standardize the connector's source-specific collection logic; it standardizes only the runtime contract around bindings, scope, state, and emitted messages. A connector that collects data via browser automation and one that calls a platform's export API both use the same START/RECORD/STATE/DONE protocol, the same binding matching, and the same state management.
This abstraction is intentional. Many platforms do not currently offer structured data portability APIs. The browser_automation binding enables connectors that drive a browser to collect data from a platform's web UI. As platforms adopt data portability standards or offer their own APIs, connector implementations can change without changing the consent surface, grant enforcement, or query API.
Requirements Language
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this profile are to be interpreted as described in the core spec's Requirements Language (BCP 14 [RFC 2119] [RFC 8174]) when, and only when, they appear in all capitals.
1. Connector Manifest Extensions
The core manifest (Section 7 of the core spec) defines the consent surface. The Collection Profile adds execution-specific fields.
{
"protocol_version": "0.1.0",
"connector_id": "https://registry.pdpp.org/connectors/spotify",
"version": "2.0.0",
"display_name": "Spotify",
"runtime_requirements": {
"bindings": {
"network": { "required": true },
"interactive": { "required": true }
}
},
"capabilities": {
"human_interaction": ["credentials", "otp"]
},
"streams": [
{
"name": "top_artists",
"incremental": true
}
]
}Collection-specific manifest fields
| Field | Description |
|---|---|
runtime_requirements.bindings | Declared bindings the connector requires from the runtime. Keys are binding names; values are objects with required: boolean and optional binding-specific fields. Standard bindings are listed below. Extension bindings use namespaced identifiers (e.g., nvidia.com/gpu). Unqualified binding names are reserved for the spec-defined registry. |
capabilities.human_interaction | Interaction kinds this connector may request: credentials, otp, manual_action. |
streams[].incremental | Whether this stream supports cursor-based incremental sync. |
streams[].state_stream | Optional. Names exactly one other declared stream whose committed checkpoint governs this stream. Requires streams[].coverage_strategy: "checkpoint_window" on the same stream. See Checkpoint dependency below. |
streams[].parent_streams | Optional. Names one or more other declared streams whose checkpoints this stream's detail evidence can independently satisfy. Requires streams[].coverage_strategy: "parent_detail_accounting" on the same stream. See Checkpoint dependency below. |
streams[].coverage_strategy | Optional. Declares how a stream's coverage is accounted. This profile constrains only the two values that interact with checkpoint dependency, checkpoint_window and parent_detail_accounting; other values a runtime may define are outside this profile's normative scope. |
A stream declaration MUST NOT set both state_stream and parent_streams. A stream with neither field is its own checkpoint parent (self-mapped): its own STATE messages govern its own commit eligibility, exactly as in a manifest that predates this section.
Checkpoint dependency
Some streams do not carry their own cursor. A stream that rides another stream's checkpoint (for example, a reactions or attachment-metadata stream co-emitted alongside the messages that reference it) declares that relationship in the manifest so the runtime can compute which staged STATE commits are safe to persist when a run ends in a certified stream-scoped failure (see DONE) or reports incomplete detail coverage (see DETAIL_COVERAGE).
There are two declaration shapes, and a stream MUST use at most one. Each shape REQUIRES a specific coverage_strategy value on the same stream; a runtime MUST reject a manifest declaring state_stream or parent_streams with any other (or absent) coverage_strategy value on that stream (see Validation rule 7 below):
state_stream(single parent).streams[].state_streamis a string naming exactly one other declared stream, and REQUIREScoverage_strategy: "checkpoint_window"on the declaring stream. It declares a static one-to-one mapping: this stream is always covered by that one parent's checkpoint, and the connector never emitsDETAIL_COVERAGEfor it — the runtime projects its checkpoint status from the parent's commit outcome directly. Use this shape for a stream that has no independent hydration lane of its own.parent_streams(one or many parents).streams[].parent_streamsis a non-empty array naming one or more other declared streams, and REQUIREScoverage_strategy: "parent_detail_accounting"on the declaring stream. It declares that this stream runs its own list+detail hydration lane and emits oneDETAIL_COVERAGEmessage per parent boundary per run (see DETAIL_COVERAGE); each parent's checkpoint is gated independently by its own coverage report, gap accounting, and the union of runtime-observed parents (see Precedence between manifest and run-time evidence). Use this shape for a detail stream that can be fed by more than one independently checkpointed list stream in the same run, or that proves its own coverage rather than inheriting a parent's outcome unconditionally.
Validation
A conformant runtime MUST validate every stream's checkpoint-dependency declaration before spawning the connector, and MUST reject the manifest (fail closed, run not started) if any of the following holds:
- Self-reference.
state_streamequals the declaring stream's ownname, orparent_streamscontains the declaring stream's ownname. - Unknown stream.
state_stream, or any entry ofparent_streams, names a stream not present inmanifest.streams. - Duplicate parent.
parent_streamscontains the same stream name more than once. - Both fields present. A stream declares both
state_streamandparent_streams. A runtime MUST enforce this directly (as its own explicit check) rather than relying solely onstate_streamandparent_streamsbeing gated to mutually exclusivecoverage_strategyvalues as an incidental side effect — the two fields being individually valid only under differentcoverage_strategyvalues does not excuse a runtime from also rejecting a manifest that sets both. - Empty
parent_streams.parent_streamsis present but has zero entries. (Omit the field entirely for a self-mapped stream.) - Cycle (forward-compatibility floor; not implemented as separate detection logic by the reference implementation — see non-normative notes below). Following
state_stream/parent_streamsedges from any stream, by any path, returns to that same stream. A cycle makes the dependency graph unresolvable to a topological commit order and MUST be rejected at manifest validation, not discovered at run time, by any runtime that resolves multi-level (transitive) parent chains. coverage_strategymismatch. A stream declaresstate_streamwithoutcoverage_strategy: "checkpoint_window"on that stream, or declaresparent_streamswithoutcoverage_strategy: "parent_detail_accounting"on that stream.
A manifest that passes this validation MUST have every stream's checkpoint-dependency edges forming a directed acyclic graph, terminating in one or more streams that are self-mapped (own checkpoint parent).
Non-normative notes:
- These are direct, single-level declarations — a stream names its parent(s) directly, not through a transitive chain it expects the runtime to resolve. A runtime MAY additionally reject a manifest whose declared edges are more than one level deep (e.g., stream C names parent B, and B itself names parent A) if its implementation does not resolve transitive chains; the reference implementation validates exactly one level and does not resolve chains beyond it.
- Rule 6 (cycle rejection) is stated as a normative floor for any runtime that resolves multi-level parent chains. The reference implementation validates only direct, one-level parent declarations (rules 1–5, 7); at that resolution depth, no cycle longer than a direct self-reference (already rejected by rule 1) is constructible through the validated fields, so the reference implementation has no separate cycle-detection code path and no cycle-specific conformance test — its rule-1/rule-2 checks make rule 6 vacuously true today. A runtime that implements transitive multi-level parent resolution MUST implement and test genuine cycle detection; a runtime that, like the reference implementation, resolves only one level MAY rely on rules 1–5 to make rule 6 unreachable rather than implementing separate graph-cycle-detection logic.
Standard bindings
| Binding | Descriptor | Meaning |
|---|---|---|
browser_automation | { interface: "cdp", ws_url: string, headed_supported?: boolean } | Runtime provides a CDP WebSocket to a managed browser. |
browser_profile | { profile_path: string } | Runtime provides a persistent browser profile directory. |
filesystem | {} | Presence indicates local filesystem access. |
network | {} | Presence indicates outbound network access. |
interactive | {} | Presence indicates INTERACTION messages will be handled. |
loopback_listen | {} | Presence indicates the connector may bind to local ports. |
2. Connector Run Protocol
Connectors communicate with the runtime via newline-delimited JSON (JSONL) over stdin/stdout. Each message is a single JSON object followed by a newline.
Runtime binding matching
Before spawning a connector, the runtime checks the manifest's runtime_requirements.bindings against its own capabilities. If the runtime cannot satisfy a required binding, the run MUST fail with a clear error before the connector process is spawned. This follows the Kubernetes scheduler pattern: connectors declare requirements, runtimes advertise capabilities.
Connector process state machine
The connector process transitions through the following states:
| State | Description |
|---|---|
initializing | Before START is received on stdin. |
collecting | Emitting RECORD, STATE, SKIP_RESULT, PROGRESS messages. |
waiting_for_interaction | Emitted INTERACTION; blocked waiting for INTERACTION_RESPONSE on stdin. |
succeeded | Emitted DONE with status: "succeeded". Terminal. |
failed | Emitted DONE with status: "failed", or exited with non-zero status. Terminal. |
State transition table:
| Current State | Event | Action | Next State |
|---|---|---|---|
initializing | START received | Initialize collection | collecting |
collecting | Emit INTERACTION | Write to stdout; block on stdin | waiting_for_interaction |
collecting | Emit DONE (succeeded) | Write to stdout; exit 0 | succeeded |
collecting | Emit DONE (failed) | Write to stdout; exit non-zero | failed |
collecting | Fatal error | Write to stderr; exit non-zero | failed |
collecting | INTERACTION_RESPONSE received | Protocol violation (see below) | failed |
waiting_for_interaction | INTERACTION_RESPONSE received | Unblock; process response | collecting |
waiting_for_interaction | Emit INTERACTION | Protocol violation (see below) | failed |
waiting_for_interaction | Fatal error | Write to stderr; exit non-zero | failed |
| Any | Runtime terminates process | (external) | failed |
Protocol violations:
- A connector MUST NOT emit INTERACTION while already in
waiting_for_interaction. A runtime that receives a second INTERACTION in this state MUST terminate the connector process and mark the run as failed. Note (non-normative): Runtimes that process connector messages sequentially via a single-threaded message queue may make this violation unrepresentable in practice, because the queue serializes INTERACTION processing. The protocol rule remains valid for correct connector behavior and for runtime architectures that dispatch messages concurrently. - A connector that receives INTERACTION_RESPONSE while in
collecting(no pending INTERACTION) SHOULD treat it as a fatal protocol error, write a diagnostic to stderr, and exit with non-zero status. - START is exactly-once. It MUST be the first message sent by the runtime. A connector that receives START while in any state other than
initializingMUST treat it as a fatal protocol error.
Runtime behavior on failure: The runtime MUST NOT persist STATE checkpoints from a run that terminates in the failed state, except for the certified stream-scoped failure described under DONE. State is otherwise persisted only after a successful DONE.
SKIP_RESULT is a message emitted while in the collecting state. It does not cause a state transition.
3. Messages
Runtime to Connector
START
Initializes a collection run.
{
"type": "START",
"run_id": "run_abc123",
"collection_mode": "incremental",
"scope": {
"streams": [
{
"name": "top_artists",
"time_range": {
"since": "2025-10-11T00:00:00Z"
},
"fields": [
"id",
"name",
"genres",
"popularity",
"source_updated_at"
]
}
]
},
"state": {
"top_artists": { "last_updated": "2026-03-01T00:00:00Z" }
},
"bindings": {
"browser_automation": {
"interface": "cdp",
"ws_url": "ws://127.0.0.1:39011/devtools/browser/abc"
},
"network": {}
}
}| Field | Type | Description |
|---|---|---|
run_id | string | Unique identifier for this run. |
collection_mode | enum | full_refresh or incremental. Derived from stream capabilities and runtime policy; not from the grant. |
scope | object | Portable collection target for this run. Derived from a grant and local policy for grant-driven runs, or from user preferences and local policy for proactive runs. See scope fields below. |
state | object or null | Map of stream names to cursor objects from previous STATE messages. For proactive runs this comes from the connector's global state namespace; for continuous grant runs it comes from the grant_id-scoped namespace; null on first run or single_use runs. |
bindings | object | Map of binding names to descriptors for bindings provided to this run. |
The START message does not include the raw grant or access token. It carries a normalized scope object instead. scope is not itself a grant and has no authorization force; it is the collection target for this run. For grant-driven runs, the runtime MUST derive scope from the grant, MUST NOT construct a scope broader than the grant permits, and MAY narrow it further according to local fulfillment policy (for example, collecting only the stale streams needed to satisfy the current request). For proactive runs, the runtime derives scope from user preferences or local policy.
scope fields
| Field | Type | Description |
|---|---|---|
streams | CollectionStream[] | Explicit stream targets for this run. MUST be non-empty. Wildcards are not allowed in START; the runtime resolves them before spawning the connector. |
CollectionStream fields
| Field | Type | Description |
|---|---|---|
name | string | Stream name to collect. |
resources | string[] | Optional canonical key strings limiting the run to specific records within the stream. Same encoding as resources in the core grant model. |
time_range | object | Optional temporal collection window with since / until, using the same semantics as the core grant model. |
fields | string[] | Optional top-level emitted-field set for this run. When present, the runtime MUST include any schema-required fields and any additional top-level fields required for valid RECORD emission or RS ingest validation for that stream. |
START.scope carries normalized collection targets only. It does not include issuance-time concepts such as necessity or unresolved view names; the runtime resolves those before spawning the connector.
Connector obligations for scope:
- A connector MUST NOT emit RECORD messages for streams absent from
scope.streams. - If
resourcesortime_rangeis present for a stream, the connector MUST apply those constraints before emitting RECORD messages for that stream. - If
fieldsis present for a stream, the connector MUST NOT emit additional top-level fields in RECORDdatafor that stream, except that it MAY include schema-required or ingest-required top-level fields if the runtime omitted them accidentally. - A connector that cannot honor a declared
resources,time_range, orfieldsconstraint for a stream MUST either emitSKIP_RESULTwithreason: "scope_not_supported"and omit records for the skipped target, or fail the run. It MUST NOT silently broaden or ignore the constraint. - A connector MAY retrieve broader source-side data transiently when the source platform cannot filter precisely, but it MUST still emit RECORD messages consistent with
scope.
Connector compliance is not the only enforcement backstop. The runtime and downstream write path MUST reject or discard emissions that fall outside the declared scope.
State management: State is maintained at two levels:
- Global state: Used and advanced only by proactive runs (no grant). Represents archival completeness for the user's data store.
- Grant-scoped state: Used and advanced by
continuousgrant runs, keyed bygrant_id. The runtime reads and writes this namespace throughGET/PUT /v1/state/{connector_id}?grant_id={grant_id}. It ensures recurring app syncs are incremental without interfering with global archival cursors. - Single-use runs: Receive
state: null. STATE messages emitted during single-use runs are not persisted.
bindings contains a descriptor for every binding declared required: true in the manifest. For every required binding, the runtime MUST include a valid descriptor. Connectors MUST treat a missing required binding as a fatal protocol error. Connectors MUST ignore unknown binding keys.
INTERACTION_RESPONSE
Reply to an INTERACTION request.
{
"type": "INTERACTION_RESPONSE",
"request_id": "req_001",
"status": "success",
"data": { "email": "user@example.com", "password": "..." }
}| Field | Type | Description |
|---|---|---|
request_id | string | Matches the request_id from the INTERACTION being answered. |
status | enum | success, cancelled, or timeout. |
data | object | Response data. Present only when status is success. |
On timeout, the runtime MUST send a response with status: "timeout" rather than leaving the connector blocked indefinitely.
Connector to Runtime
RECORD
A single data record. Same envelope as the core spec (Section 4).
{
"type": "RECORD",
"stream": "top_artists",
"key": "4Z8W4fKeB5",
"data": {
"id": "4Z8W4fKeB5",
"name": "Radiohead",
"genres": ["alternative rock"],
"popularity": 82,
"source_updated_at": "2026-03-28T00:00:00Z"
},
"emitted_at": "2026-04-06T15:01:00Z"
}The op field (upsert or delete) is a directive to the resource server and is not stored as part of the record data.
STATE
Checkpoint for incremental sync.
{
"type": "STATE",
"stream": "top_artists",
"cursor": { "last_updated": "2026-03-28T00:00:00Z" }
}The runtime persists STATE only after preceding records are durably written to the resource server. Connectors SHOULD emit STATE periodically (e.g., every 1000 records) rather than only at the end of a stream.
stream names the checkpoint (state_stream), which can differ from the data stream(s) it covers. A stream is its own checkpoint unless the manifest declares state_stream or parent_streams for it (see Checkpoint dependency). Every STATE message's stream MUST be a checkpoint stream: either a data stream with no declared parent (self-mapped), or a stream named as another stream's state_stream/parent_streams target. A checkpoint stream's commit eligibility depends on the coverage and failure evidence of every data stream mapped to it — see Eligible-checkpoint algorithm.
The cursor object is opaque to the runtime and the resource server: its structure is defined by the connector and interpreted only by the connector on the next run.
INTERACTION
Request input from a user or agent. The connector blocks (does not emit further messages) until INTERACTION_RESPONSE arrives on stdin.
{
"type": "INTERACTION",
"request_id": "req_001",
"kind": "credentials",
"message": "Log in to Spotify",
"schema": {
"type": "object",
"properties": {
"email": { "type": "string" },
"password": { "type": "string", "format": "password" }
},
"required": ["email", "password"]
},
"timeout_seconds": 300
}| Kind | When to use |
|---|---|
credentials | Username/password login form. |
otp | Two-factor authentication or verification code. |
manual_action | An action the user must take in a headed browser (login, CAPTCHA, confirmation). |
SKIP_RESULT
Signals that a stream or resource was intentionally skipped. Does not cause a state transition.
{
"type": "SKIP_RESULT",
"stream": "playlists",
"reason": "rate_limited",
"message": "Skipped playlists: rate limit reached"
}SKIP_RESULT MAY also be used when a connector cannot honor a declared scope element for a stream or resource. In that case the reason MUST be scope_not_supported.
SKIP_RESULT MAY carry an optional recovery_hint. See Recovery hints below for its shape and validation rules — the same rules apply here as for DONE.error.recovery_hint.
SKIP_RESULT MAY carry an optional typed continuation fact when a bounded
page completed and the runtime owns the next page. It MUST contain
boundary, slice_start, slice_end, considered, covered,
remaining: true, and owner: "runtime". The counts bind the continuation to
the exact proven page; a runtime MUST NOT treat an ordinary retryable skip as a
healthy continuation merely because its separate coverage denominator is full.
The fact proves only that slice. It MUST NOT imply complete history.
DETAIL_COVERAGE
Reports how completely a connector hydrated per-record detail for one checkpoint-parent boundary in a list+detail collection lane (a lane that fetches a list of records and then fetches per-record detail for at least a subset of them). Does not cause a state transition.
{
"type": "DETAIL_COVERAGE",
"reference_only": true,
"stream": "message_attachments",
"state_stream": "messages",
"required_keys": ["msg_1", "msg_2", "msg_3"],
"hydrated_keys": ["msg_1", "msg_2"],
"gap_keys": ["msg_3"],
"optional_skip_keys": []
}| Field | Type | Description |
|---|---|---|
reference_only | true | MUST be present and true. Marks this message as evidence about a run, not itself durable data. |
stream | string | The detail stream this report covers. MUST be present in scope.streams. |
state_stream | string | The checkpoint-parent stream this report's evidence gates. MUST be present in scope.streams. |
required_keys | (string | number)[] | The full set of record keys considered for detail under this parent boundary in this run. |
hydrated_keys | (string | number)[] | The subset of required_keys successfully fetched and emitted as RECORD. |
gap_keys | (string | number)[] | Optional. Keys for which a DETAIL_GAP was emitted this run. |
optional_skip_keys | (string | number)[] | Optional. Keys the connector accepts as terminally unavailable under an explicit optional-detail policy (see below). |
A connector with a list+detail lane MUST emit one DETAIL_COVERAGE message per distinct state_stream boundary after that boundary's detail work for the run settles, and MUST place it after the last RECORD or DETAIL_GAP it emits for that detail stream and boundary in the run. A connector exempt from a per-record detail fetch (flat streams only) is not required to emit DETAIL_COVERAGE.
Non-normative note: this ordering rule is a conformance obligation on the connector; the reference implementation does not currently enforce message sequence/ordering for DETAIL_COVERAGE at the runtime level (it accepts the message whenever it arrives and evaluates coverage as of terminal DONE). A runtime MAY choose to validate ordering explicitly; the eligible-checkpoint algorithm's correctness in the reference implementation does not depend on runtime-side ordering enforcement, only on the connector honestly reporting complete state by the time DONE is evaluated.
Key-set validation. Within one DETAIL_COVERAGE message:
required_keys,hydrated_keys,gap_keys, andoptional_skip_keysMUST each contain no duplicate key.- Every key in
hydrated_keys,gap_keys, oroptional_skip_keysMUST also appear inrequired_keys. - A single key MUST NOT appear in more than one of
hydrated_keys,gap_keys,optional_skip_keyswithin the same message. - A key in
required_keysthat appears in none of the three outcome sets is an unaccounted key: its parent boundary's coverage is incomplete (see Eligible-checkpoint algorithm).
Multi-parent streams. A detail stream declared with manifest parent_streams (see Checkpoint dependency) MAY emit more than one DETAIL_COVERAGE message in the same run — one per parent boundary that settled. The runtime MUST evaluate and gate each parent's checkpoint independently from the others' coverage; it MUST NOT reject two DETAIL_COVERAGE messages solely because they share the same stream while their state_stream values differ.
optional_skip_keys evidence bar. A connector MAY place a required key in optional_skip_keys only when both of the following hold:
- The manifest's stream contract makes that detail optional for the declaring stream (an accepted-absence coverage declaration, reference-implementation-specific in shape but required in substance).
- A connector-specific check affirmatively identifies a terminal, source-confirmed absence of that detail (for example, a provider's own "this object no longer exists" response), rather than an ambiguous failure.
An HTTP status code alone, response age alone, transport failure, retry exhaustion, or a generic access-denied response MUST NOT by itself establish terminal unavailability. A connector that cannot affirmatively clear this bar MUST leave the key out of all three outcome sets (unaccounted) rather than placing it in optional_skip_keys; an unaccounted key withholds its parent's checkpoint and is retried on the next run, which is the fail-closed default.
DETAIL_GAP
Reports a durable, retryable per-record detail failure within a list+detail lane. Does not cause a state transition.
{
"type": "DETAIL_GAP",
"stream": "message_attachments",
"parent_stream": "messages",
"record_key": "msg_3",
"reason": "temporary_unavailable",
"retryable": true,
"detail_locator": { "message_id": "msg_3" }
}| Field | Type | Description |
|---|---|---|
stream | string | The detail stream the failing record belongs to. MUST be present in scope.streams. |
parent_stream | string | Optional. The checkpoint-parent boundary this gap is scoped to. MUST match a DETAIL_COVERAGE.state_stream value the connector reports for this stream in the same run when the detail stream has more than one declared parent (see below). |
record_key | string | number | Optional. The detail record's key within stream. |
reason | string | Optional. Connector-defined failure reason. |
retryable | boolean | Optional. Whether the connector considers this gap retryable. |
detail_locator | object | Optional. Connector-opaque data sufficient to retry this detail fetch independently on a future run. MUST NOT contain secrets. |
list_cursor, last_error | object | Optional connector-opaque diagnostic objects. |
gap_id, lease_id | string | Optional. Identify a durable, runtime-served gap-recovery lease when the runtime's out-of-band gap-recovery mechanism is in use. Reference-implementation-specific; not required for profile conformance. |
Parent scoping and key collision. A DETAIL_GAP names the checkpoint boundary it accounts for via parent_stream. For a stream with exactly one declared parent (state_stream, or a single-entry parent_streams), parent_stream MAY be omitted; the runtime MUST treat the omission as naming that one parent. For a stream with more than one declared parent, the runtime MUST treat a DETAIL_GAP with no parent_stream, or with a parent_stream not matching the coverage report being evaluated, as not accounting for that report's required key — even when the stream and record_key match exactly. The same detail key can be legitimately gapped under one parent while hydrated or covered under a different parent in the same run; a gap recorded against one parent MUST NOT satisfy another parent's coverage. A DETAIL_GAP emitted before this section's parent-scoping rule existed (no parent_stream) satisfies coverage only for a stream that has exactly one declared parent; it MUST NOT be treated as satisfying any one parent of a stream with more than one declared parent.
A gap_keys entry in DETAIL_COVERAGE is not by itself proof of a durable retry obligation. The runtime MUST additionally confirm a matching DETAIL_GAP exists for the same stream and parent boundary before crediting that key as accounted; an unmatched gap_keys entry leaves the key unaccounted for coverage purposes, with the same fail-closed effect as a key omitted from every outcome set.
PROGRESS
Optional progress update for display in runtime UIs.
{
"type": "PROGRESS",
"stream": "messages",
"message": "Downloaded 500 of 2196 messages",
"count": 500,
"total": 2196
}DONE
Signals completion. Must be the final message emitted by the connector.
{
"type": "DONE",
"status": "succeeded",
"records_emitted": 2196
}On failure:
{
"type": "DONE",
"status": "failed",
"records_emitted": 0,
"error": { "message": "Authentication failed", "retryable": true }
}| Status | Meaning |
|---|---|
succeeded | Collection completed. Runtime persists final STATE. |
failed | Collection failed. Runtime does not persist STATE unless the messages certify a stream-scoped failure as described below. |
cancelled | Collection was cancelled (e.g., user revoked mid-run). Runtime does NOT persist STATE. |
A failed run certifies a stream-scoped failure only when both of these conditions hold:
DONE.error.codeisstream_collection_failed.- The run previously emitted at least one in-scope
SKIP_RESULTwithreason: "stream_collection_failed"and a non-emptystreamnaming each failed data stream.
A runtime MUST verify condition 2 structurally (a named, in-scope SKIP_RESULT was actually observed this run) and MUST NOT treat DONE.error.code alone as certification. If condition 1 holds but no in-scope SKIP_RESULT{reason: "stream_collection_failed"} was observed, the run is an ordinary uncertified failure and the default fail-closed rule applies: no staged STATE is persisted.
Cancellation precedence. If the runtime has recorded an owner-initiated cancellation for the run (for example, a mid-run revocation), the runtime MUST resolve the run as cancelled and MUST NOT evaluate or apply the stream-scoped-failure exception, even if the connector emitted a structurally certified DONE{status: "failed", error.code: "stream_collection_failed"} before the runtime observed the cancellation. Cancellation is checked, and takes precedence, before a terminal DONE is evaluated for certification.
Eligible-checkpoint algorithm
For a certified stream-scoped failure, the runtime MAY persist staged STATE for checkpoint streams that do not cover any named failed data stream. A runtime that implements this exception MUST compute eligibility as follows, run against the run's complete staged STATE map (every checkpoint stream for which a STATE message was received this run):
- Resolve each failed data stream to its checkpoint stream(s). For each data stream named by an in-scope
SKIP_RESULT{reason: "stream_collection_failed"}, compute its set of checkpoint parents using the same resolution order as Precedence between manifest and run-time evidence: prefer the union ofstate_streamvalues from this run'sDETAIL_COVERAGEmessages naming that data stream asstream; if none were emitted this run, fall back to the manifest's static declaration (state_streamorparent_streams) for that stream; if neither exists, the stream is self-mapped (its own name is its one checkpoint parent). - Union every failed data stream's checkpoint parents into one set of ineligible checkpoint streams.
- Compute detail-coverage shortfalls independently of the failure. For every staged checkpoint stream, evaluate every
DETAIL_COVERAGEreport gating it: a report is incomplete if anyrequired_keysentry is unaccounted (present in none ofhydrated_keys,optional_skip_keys, or agap_keysentry backed by a matchingDETAIL_GAPfor the same stream and parent boundary — see DETAIL_GAP), or if a manifest-declared parent relationship for an in-scope detail stream has noDETAIL_COVERAGEreport at all this run. Add every checkpoint stream with an incomplete report to the ineligible set. - Commit every staged checkpoint stream not in the ineligible set. The runtime MUST NOT persist STATE for any checkpoint stream in the ineligible set (from step 2 or step 3).
- Partial checkpoint-store failure. If persisting an individual eligible checkpoint stream's STATE fails after the eligibility set is computed (for example, a resource-server write error), the runtime MUST fail the run as a runtime error. A partial commit failure MUST NOT be reported as
succeeded, and any checkpoint stream not yet committed at the point of failure remains eligible for retry on the next run. The runtime MUST make the following observable, though not necessarily from a single field or object: the total count of checkpoint streams staged and the total count durably committed before the failure (a bounded numeric summary; this MAY be all a single terminal-result field exposes), the identity of the specific checkpoint stream whose persistence attempt failed (for example, in a diagnostic message or a dedicated failure event), and the identity of each checkpoint stream that was durably committed before the failure (for example, via a per-stream commit event emitted at the time of that commit, or by reading back the durably persisted state after the run). A runtime is not required to expose a single response field naming every staged-and-committed stream together; it MUST NOT expose only bounded counts with no path at all to recovering which specific streams committed.
The run's own status remains failed regardless of how many checkpoint streams commit under this exception; every named failed data stream, and every checkpoint stream withheld under steps 2–3, remains unproven and eligible for retry on the next run.
A missing or mismatched terminal code, a missing or untargeted skip, an out-of-scope stream, a protocol violation, an invalid terminal count or exit code, a process exit without valid DONE, or cancellation MUST preserve the default fail-closed rule and persist no staged STATE.
error MAY carry code and/or recovery_hint, in addition to the required message and retryable:
codeis a stable, connector-defined cause identity (e.g. distinguishing one failure mode from another). It is a boundedsnake_caseidentifier (a lowercase letter followed by up to 63 lowercase letters, digits, or underscores), an identity rather than an instruction — the runtime MUST NOT treatcodeas, or derive, an owner-facing recovery action from it.recovery_hintis the connector's declaration of the owner-facing recovery action. It uses the exact same bounded shape and vocabulary asSKIP_RESULT.recovery_hint— see Recovery hints.
code and recovery_hint answer different questions (what went wrong vs. what to do about it) and MUST be validated and consumed independently; a runtime MUST NOT infer one from the other.
Precedence between manifest and run-time evidence
A data stream's checkpoint parent(s) can be declared two ways: statically in the manifest (state_stream or parent_streams), or asserted at run time by the connector's own DETAIL_COVERAGE messages (state_stream field, per message). A runtime MUST resolve a data stream's checkpoint parent(s) for a given run in this order:
- Live run-time evidence takes precedence when present. If the connector emitted one or more
DETAIL_COVERAGEmessages naming this data stream asstreamin the current run, the stream's checkpoint parents for this run are the union of everystate_streamvalue across those messages — regardless of what the manifest declares. - Manifest declaration is the fallback. If the connector emitted no
DETAIL_COVERAGEfor this data stream in the current run, the stream's checkpoint parent is the manifest's static declaration:state_stream(single parent) orparent_streams(the full declared set, union'd). - Self-mapping is the default. If neither applies, the stream's own name is its one checkpoint parent.
This resolution order means live evidence for a run can supersede — but never contradict in a way the runtime silently accepts — the manifest's static shape. A manifest declaring parent_streams: ["a", "b"] MAY see the connector's live evidence for a given run name only "a" (for example, if "b"'s boundary produced no detail this run); the runtime treats that run's checkpoint dependency as only "a", without this being a conflict, because manifest and live evidence describe different scopes (the connector's declared capability vs. this run's observed behavior). There is no conflict-rejection case for manifest vs. run-time evidence: rule 1 unconditionally wins over rule 2 when live evidence exists for that stream in that run. A manifest declaring state_stream (single-parent shape) for a stream that then emits DETAIL_COVERAGE with a different state_stream value is not a manifest/runtime conflict either — rule 1 still applies, using the live value; a manifest author who intends a stream to run its own hydration lane with a coverage-driven parent should declare it with parent_streams, not state_stream, to avoid an unused static declaration.
Recovery hints
SKIP_RESULT.recovery_hint and DONE.error.recovery_hint share one bounded, provider-neutral shape and vocabulary:
recovery_hintis either a bare string from the closed action vocabulary below, or an object{ action: string, retryable?: boolean }whereactionMUST be present and from that vocabulary, andretryable, if present, MUST be a boolean.- An empty object
{}or an object with onlyretryablefield is a protocol violation: if a connector supplies a recovery hint as an object, theactionfield is mandatory. - Action vocabulary:
retry_by_runtime,retry_on_connector_upgrade,refresh_credentials,manual_action_required,update_selector,upstream_unblock,not_retriable,unknown. - A connector requests a specific owner-facing recovery action only through
recovery_hint. A present, validrecovery_hintis authoritative: a runtime MUST NOT override it, and MUST NOT treatcode,message, or any other connector-authored free-form text as the connector's requested action. - A runtime MUST treat an absent
recovery_hintas "no hint declared," and MAY fall through to its own generic, connector-neutral policy for choosing a default action — for example from theretryableflag, or from bounded, provider-neutral classification of the error text (such as recognizing generic authentication or browser-infrastructure failures). That fallback MUST NOT infer provider-specific intent, and MUST NOT be, or become, a connector-specific text/identity heuristic. - A
recovery_hintthat is present but does not match the shape or vocabulary above is a protocol violation: the runtime MUST reject the enclosing message (fail closed), not silently drop the field or substitute a guessed action.
4. Connector Conformance
A conformant connector:
- Reads START from stdin before emitting any messages.
- Emits only valid JSONL messages as defined in this profile.
- Emits DONE as the final message in all cases (including failures where possible).
- Emits STATE periodically for streams that support incremental sync.
- Does not store secrets (credentials, OTP codes) in STATE.
- Does not emit INTERACTION while in
waiting_for_interaction. - Treats missing required bindings as fatal errors.
- Exits with status 0 on
succeeded, non-zero onfailedorcancelled. - Emits RECORD messages only within the
scopeprovided in START: no undeclared streams, no records outside declaredresourcesortime_range, and no extra top-level fields whenfieldsis present. - If it cannot honor a declared
resources,time_range, orfieldsconstraint, emits an explicitSKIP_RESULTor fails the run; it never silently broadens scope. - If it runs a list+detail lane, emits
DETAIL_COVERAGEper checkpoint-parent boundary per run, with every required key accounted inhydrated_keys,gap_keys, oroptional_skip_keys, and only places a key inoptional_skip_keyswhen it has affirmatively confirmed terminal unavailability (see DETAIL_COVERAGE). - Scopes every
DETAIL_GAPto the checkpoint-parent boundary it accounts for viaparent_streamwhenever the affected detail stream has more than one declared parent.
A conformant connector runtime:
- Performs binding matching before spawning the connector process.
- Sends START as the first and only START message.
- Handles INTERACTION messages by prompting the user or agent and sending INTERACTION_RESPONSE.
- Sends INTERACTION_RESPONSE with
status: "timeout"if no response arrives withintimeout_seconds. - Persists STATE only after preceding records are durably written.
- Does NOT persist STATE on
cancelledruns or uncertifiedfailedruns; for a certified stream-scoped failure, persists only staged checkpoint streams computed eligible under the Eligible-checkpoint algorithm. Checks for and honors an owner-initiated cancellation before evaluating a terminal DONE for stream-scoped-failure certification. - Uses the connector's global state namespace for proactive runs, the
grant_id-scoped namespace forcontinuousgrant runs, andstate: nullforsingle_useruns. - Terminates the connector process on protocol violations.
- Does not log or persist credential data from INTERACTION_RESPONSE.
- Sends an explicit non-empty
scopein START. For grant-driven runs, this scope is a normalized, possibly narrowed projection of the grant and MUST NOT include wildcard stream names. - For grant-driven runs, never constructs a
scopebroader than the grant permits. - Rejects or discards connector emissions that fall outside the declared
scopebefore durable write. - Validates every stream's checkpoint-dependency declaration (
state_stream/parent_streams) before spawning the connector, and fails closed (does not start the run) on self-reference, an unknown parent, a duplicate parent, both fields present on one stream, or an emptyparent_streams(see Checkpoint dependency: Validation). A runtime that resolves only direct, one-level parent declarations — as the reference implementation does — satisfies the cycle-rejection rule (rule 6) vacuously through these same checks and is not required to implement separate cycle-detection logic; a runtime that resolves transitive multi-level parent chains MUST implement and test genuine cycle detection (see the non-normative notes under Validation). - Resolves a data stream's checkpoint parent(s) for a run using live
DETAIL_COVERAGEevidence when present, falling back to the manifest declaration, falling back to self-mapping (see Precedence between manifest and run-time evidence). - Fails the run as a runtime error, without reporting
succeeded, if persisting an eligible checkpoint's STATE fails partway through committing multiple staged checkpoints; makes the staged/committed counts, the failing checkpoint stream's identity, and each committed checkpoint stream's identity observable (not necessarily from one field — see Eligible-checkpoint algorithm step 5).
5. TypeScript Types
type InteractionKind = 'credentials' | 'otp' | 'manual_action';
type StreamState = Record<string, Record<string, unknown>>;
type TimeRange = { since?: string; until?: string };
type CollectionStream = {
name: string;
resources?: string[];
time_range?: TimeRange;
fields?: string[];
};
type CollectionScope = {
streams: CollectionStream[];
};
type ManifestStream = {
name: string;
incremental?: boolean;
// A stream MUST declare at most one of state_stream / parent_streams.
// Neither present means the stream is its own checkpoint (self-mapped).
state_stream?: string;
parent_streams?: string[]; // non-empty when present
[key: string]: unknown;
};
type RuntimeMessage =
| {
type: 'START';
run_id: string;
collection_mode: 'full_refresh' | 'incremental';
scope: CollectionScope;
state: StreamState | null;
bindings: Record<string, Record<string, unknown>>;
}
| {
type: 'INTERACTION_RESPONSE';
request_id: string;
status: 'success' | 'cancelled' | 'timeout';
data?: Record<string, unknown>;
};
type ConnectorMessage =
| {
type: 'RECORD';
stream: string;
key: string | string[];
data: Record<string, unknown>;
emitted_at: string;
op?: 'upsert' | 'delete';
}
| {
type: 'STATE';
stream: string;
cursor: Record<string, unknown>;
}
| {
type: 'INTERACTION';
request_id: string;
kind: InteractionKind;
message: string;
schema?: Record<string, unknown>;
timeout_seconds?: number;
}
| {
type: 'SKIP_RESULT';
stream?: string;
reason?: string;
message?: string;
recovery_hint?: RecoveryHint;
}
| {
type: 'DETAIL_COVERAGE';
reference_only: true;
stream: string;
state_stream: string;
required_keys: (string | number)[];
hydrated_keys: (string | number)[];
gap_keys?: (string | number)[];
optional_skip_keys?: (string | number)[];
}
| {
type: 'DETAIL_GAP';
stream: string;
parent_stream?: string;
record_key?: string | number;
reason?: string;
retryable?: boolean;
detail_locator?: Record<string, unknown>;
list_cursor?: Record<string, unknown>;
last_error?: Record<string, unknown>;
gap_id?: string;
lease_id?: string;
}
| {
type: 'PROGRESS';
stream?: string;
message: string;
count?: number;
total?: number;
}
| {
type: 'DONE';
status: 'succeeded' | 'failed' | 'cancelled';
records_emitted: number;
error?: { code?: string; message: string; recovery_hint?: RecoveryHint; retryable: boolean };
};
type RecoveryAction =
| 'retry_by_runtime'
| 'retry_on_connector_upgrade'
| 'refresh_credentials'
| 'manual_action_required'
| 'update_selector'
| 'upstream_unblock'
| 'not_retriable'
| 'unknown';
type RecoveryHint = RecoveryAction | { action: RecoveryAction; retryable?: boolean };Protocol Specification
Authorization and disclosure semantics for personal data — record model, selection request, grant, manifest, and resource server interface.
Extension Profile: Lexical Search
Optional companion profile to the Personal Data Portability Protocol (PDPP) core spec defining a discoverable, grant-safe lexical (full-text) search surface.