OptionalAfterKEYSET / seek resume position (plan.md §7): the last-seen value of the connector's
StableOrderingKey. The connector fetches WHERE <key> > AfterKeyValue ORDER BY <key> so a
mid-stream insert/delete cannot corrupt the scan position. Engine passes it on subsequent
calls (and on restart-recovery). undefined/null on the first page.
Maximum number of records to fetch in a single batch
The company integration entity providing connection details
User context for authorization
OptionalCurrentCurrent cursor for cursor-based pagination. Passed by engine on subsequent calls.
OptionalCurrentCurrent offset for offset-based pagination. Passed by engine on subsequent calls.
OptionalCurrentCurrent page number for page-based pagination (1-based). Passed by engine on subsequent calls.
OptionalDeadlineOptionalIsSAMPLING, NOT SYNCING — and the wall-clock this call must not outlive.
Discovery wants a corpus, not a corpus of everything: ~50 records is enough to infer columns,
types, string widths and a provable primary key. DiscoverFieldsViaFetch already knows that
and already computes a budget — but it hands that budget to the code CONSUMING the record
stream, and the consumer only regains control BETWEEN FetchChanges calls. Nothing was ever
passed to the connector itself, so a connector could not honour a budget even if it wanted to:
it had no way to know it was being sampled rather than synced.
That is survivable while one FetchChanges is one HTTP page — the consumer stops after 50
records and the gap never shows. It is NOT survivable for a parent-scoped object, where a
single call fans out internally into one request per parent. There the consumer cannot
interrupt anything, because control does not come back until every parent has been walked.
Observed live 2026-08-12: a Totara discovery spent 28 minutes inside ONE FetchChanges call,
walking every parent, and returned rows=0 — half an hour of correct, pointless work to
collect a sample it could never have found there. A sampling operation had silently become an
exhaustive one.
So the intent now travels with the call. A connector that ignores these behaves exactly as before; one that fans out internally can stop early and return what it has.
STOP ON RECORDS, NOT ON PARENTS. A child object only yields through its parents, so capping the
number of parents visited would be wrong — if the first three courses have no enrolments you
genuinely must keep walking to find fifty rows. SampleTargetRecords is therefore the primary
stop and the walk should honour it the moment it is met, whichever parent it happens to be on.
DeadlineMs is the BACKSTOP for the other case: parents that will never yield anything, where
no record count can ever be reached and only the clock can end it.
IsDiscoverySample — this call exists to characterise the shape of the data, not to move it.SampleTargetRecords — stop as soon as this many records have been collected. Enough is enough.DeadlineMs — epoch ms after which the connector should stop and return what it has.
A partial sample is the CORRECT result here: discovery infers from
whatever it gets, and returning little beats half an hour of silence.OptionalMaxExternal object name to fetch from
OptionalRateAdaptive rate-limit hooks (plan.md §7), supplied by the engine so a connector's INNER request loop (e.g. a second-layer/parent-iterated object that fires one request per parent) is governed by the SAME per-credential AIMD token bucket that paces the outer object level — instead of a fixed self-throttle that defeats concurrency and ignores 429 back-off. Optional + back-compat: a connector that ignores them behaves exactly as before; the engine omits them when unavailable.
RateLimitAcquire() — await one token from the adaptive bucket before each inner request.RateLimitReport(err?) — feed the outcome back so the rate auto-tunes (clean → ramp up, 429 → back off).MaxConcurrency — the engine's resolved in-flight cap for inner requests (>=1).OptionalRateOptionalRequestedOptional list of source field names to request from the external API. When provided, the connector should limit the returned fields to this set.
OptionalSampleCurrent watermark value for incremental fetch, or null for full fetch
Context passed to FetchChanges for incremental data retrieval