Checking a data source is current

What data_version, schema_version, and the sync timestamps actually guarantee, and how to verify a managed data source against the database or Salesforce org it mirrors.

Updated 2026-09-03

If you run a managed data source — mirrored from your own database or from a Salesforce object — you own the data and FieldScroll holds a copy. Sooner or later you will want to answer one question: is the copy right?

This page says which fields answer it, which fields look like they answer it and do not, and how to check without reading the whole source. All of it applies to both kinds of managed source. Where it says "your table", read "your Salesforce object" if that is what the source mirrors.

Which field answers which question

FieldWhat it tells you
sync_statusWhether the last sync succeeded, failed, or finished with work left over.
sync_errorWhy the last sync failed. Null when it did not.
last_synced_atWhen a sync last completed.
updated_at (on the source)When a sync last ran. Not whether it changed anything.
data_versionA full-resync signal for sources you upload by hand. Constant on managed sources.
schema_versionBumped when the column set changes.
updated_at (on a row)When FieldScroll last wrote that row.

The three below are the ones people misread.

data_version stays at 1 on a managed source, and that is correct

data_version increments when a hand-uploaded CSV replaces a source's rows wholesale. It means one thing: discard every local row and re-pull. It exists because a replaced CSV has no per-row history to follow.

A managed source never triggers it. Sync reconciles row by row: changed rows get a new updated_at, removed rows become tombstones. There is nothing to discard, so the counter never moves.

A managed source showing data_version: 1 months after creation, with hundreds of applied row changes, is healthy. It is not a stalled sync, and mobile clients are not holding a stale copy — they follow row timestamps, not this counter.

A source's updated_at proves the sync ran, not that it is current

It advances on every sync cycle whether or not a single row changed. It is a liveness signal. Read it as "a sync happened at this time" and nothing more.

This matters more than it sounds. A sync can complete, report ok, advance updated_at, and still not have applied everything it was asked to — the delete guardrail declines to remove more than half a source's rows in one pass. When that happens the source reads Needs attention rather than Error, so a monitor watching only sync_status === 'error' will not see it. See managed sources.

A row has two sets of timestamps, and only one is about your data

This is the trap most likely to make an audit report success when it should not.

  • row.created_at and row.updated_at — when the sync wrote the row.
  • row.data.updated_at — the value from your table, mirrored as an ordinary column, if your table has such a column.

Comparing your database's timestamps against the outer updated_at only tells you when FieldScroll last touched the row. That always looks recent. Compare against row.data.updated_at instead — the mirrored value.

How mobile stays current

Devices delta-sync on row updated_at, pulling everything changed since their last cursor. Deletions arrive as tombstones rather than absences, so a device can remove the row locally instead of guessing from a gap. Inserts, edits, and deletions all reach the device without any version counter moving.

A column-set change bumps schema_version, which rebuilds the local table.

Verifying a source

Three query parameters on GET /organizations/{organization_id}/data-sources/{data_source_id}/rows make this practical without walking every page: external_id, updated_after, and include_deleted.

If you audited a source before August 8, 2026, run it again. Paging through this endpoint used to return only part of a source when its rows carried a display order, which every managed source's rows do. There was no error and the next-page marker looked normal, so a walk that appeared to finish could have been missing rows — in one 25-row test, 12 came back. Any count or coverage conclusion drawn from a full walk before that date is not safe to rely on. Full walks are correct now.

Check one row you know about

external_id is the natural key for a managed source — the same key sync reconciles on, so you already hold it for every row you push. On a Salesforce source it is the Salesforce record Id.

curl -sS -H "Authorization: Bearer $FS_KEY" \
  "https://fieldscroll.app/api/v1/organizations/$ORG/data-sources/$SRC/rows?external_id=SKU-4471"

It is unique per source, so this returns at most one row. An empty data array means FieldScroll has no row with that key — which is itself an answer.

Check what has changed

updated_after takes an ISO 8601 timestamp and returns only rows written since. Verification then costs about what your change rate costs, rather than what your source size costs.

curl -sS -H "Authorization: Bearer $FS_KEY" \
  "https://fieldscroll.app/api/v1/organizations/$ORG/data-sources/$SRC/rows?updated_after=2026-08-01T00:00:00Z&include_deleted=true"

Pair it with include_deleted=true. A removed row keeps its updated_at bumped to the moment of deletion, so a deletion is a change — but without this the row simply stops appearing, and you cannot tell "removed" from "unchanged". Rows returned this way carry a non-null deleted_at.

By default removed rows are hidden and every row has deleted_at: null. If you are comparing counts against your own table, that is the behavior you want; if you are reconciling changes, it is not.

A reconciliation that works

  1. Read the source and check sync_status, sync_error, and last_synced_at. Ignore data_version.
  2. For each row you expect, request it by external_id and compare row.data against your table. Compare timestamps against row.data.updated_at, not the outer one.
  3. Ask for updated_after your last check, with include_deleted=true, and confirm the changes you made are the changes that landed — and that nothing landed which you did not make.
  4. If counts disagree and no sync reported an error, suspect the delete guardrail before suspecting the data.