Skip to main content
Each Tap has a schema: a JSON Schema for one record that the Tap emits. The schema lists the fields, and x-* annotations on the fields say what each field means to Parable: which field is the key, which version of a record wins, which field is a person’s email, and who can open the row. Other declarations sit on the Tap and on the Provider Plugin. Parable maintains every Provider Plugin. This page is the reference for what each declaration does to the tables you query. Read the declarations on the plugin of one connection:
Each item of connector.taps has schema, the Tap declarations, and responseExtraction. Your Workspace keeps its own copy of each schema as outputSchema on the Workspace Tap. Some annotations on that copy are yours to change. See Taps.

From Tap schema to SQL

  1. Plan. Parable reads the schema and plans the tables of the Tap: the main table, child tables for arrays, the columns, the merge key, the ordering column, and the layout.
  2. Check. A sync checks each record against the schema. A value that does not match lands as null, and the row records the reason in _parable_validation. A record without its merge key goes to a quarantine table instead.
  3. Merge. Parable merges each load into the tables by the merge key and the ordering column, as dataKind and deletionSemantics say.
  4. Derive. Taps that are identity directories feed providers.identity.*. Taps that carry access facts feed providers.access.*.
  5. Publish. The catalog lists each table and column, with its keys and foreign keys. You query the tables with SQL.
Tap tables shows the result in SQL: table and column names, current rows, child tables, and identity joins.

Annotations

An annotation is a key on one field of the schema, such as "x-transformDedupKey": true. Unless a section says otherwise, an annotation applies to a field of the record’s root object.

Keys and ordering

The merge key. Parable merges the main table on one column:
  1. The first field marked x-transformDedupKey, by field name.
  2. Else the outputField of responseExtraction.syntheticPrimaryKey.
  3. Else responseExtraction.primaryKey.
  4. Else a field named id.
A Tap marks one root field, and it is the field that responseExtraction.primaryKey names. A Tap that declares a dataKind must mark one. Every part of the merge key’s path is required in the schema, because a row without its merge key goes to the quarantine table. A dedup key inside a nested object identifies that object, not the row. It becomes the merge key only when the root marks no field. Gong calls_extensive keys on metaData.id this way. A child table merges on parent_pk and ordinal: the parent’s key and the element’s position in the array. A dedup key on a field of an array element names the element’s own key. It does not change the child table’s merge key. Ordering. When no field is marked x-transformOrdering, the Tap’s sync.cursorField is the ordering column, if the schema declares that field. Otherwise the table has no ordering column. When two rows have the same key, the row with the greater ordering value wins. A missing value counts as the oldest. When both values are missing, the later load wins. A child table carries its root row’s ordering column. Foreign keys. An object field marked x-transformForeignKey becomes one <field>_id column that holds the referenced row’s ID. Notion pages.created_by becomes created_by_id. A scalar field that already holds the key keeps its own column, and the annotation only records the reference. idField names the field of the target Tap that the value matches. It defaults to id.
The catalog publishes the column as a foreign key when idField is the target Tap’s key, or the person email of an identity directory. A field whose type is the Tap’s own record type is a foreign key to the same table, with no annotation: a comment’s parent comment becomes parent_id. A foreign key never crosses Providers.

Structure

Parable decides the layout of each nested field by its shape:
  • An object with at most 20 fields, at most 3 levels deep, flattens into parent__child columns. assignee.email becomes assignee__email.
  • A wider or deeper object is one struct column. Read one of its fields with profile."userType" or profile['userType'].
  • An array of objects at the top level becomes a child table, {tap}__{field}. A deeper array is one list column.
  • An array of scalars is one list column.
  • An object with no declared properties, or a map, is one JSON column.
x-json is the only layout annotation that a stored Tap schema keeps. A Tap schema that sets x-flatten, x-struct, or x-explode is refused.

Identity

Identity annotations say which fields are about a person or an account. They do not make a row unique, and they do not change the columns. A field has at most one identity role, on a scalar value. The typed form of x-transformExternalUserId links two accounts. With "by": "id", the value equals the x-transformAccountId of an account in the named Provider. With "by": "email", it equals that account’s person email. Linear users.gitHubUserId declares {"system": "github", "by": "id"}, so a Linear user and the GitHub member with that ID are one person. The bare true form names no Provider. It is a hint, and it links nothing. A plugin uses the typed form only when the Provider sets the value from a verified connection, such as an OAuth link. Only a Tap with identityDirectory: true feeds providers.identity.*. Its root table has an x-transformAccountId or an x-transformPersonEmail field. An annotation on a field of a nested object counts, in a flattened column or in a struct column. An annotation inside an array does not. When a table has two fields with one role, the field nearest the root wins.

Event layout

A field with the event_time or metadata_timestamp role has a date-time type. On a Tap with dataKind: event, each table has at most one event_time field. Parable lays out each table of an event Tap by its event time. When no field has the event_time role, a timestamp ordering column stands in. Each write is sorted by event time, so a query for one day skips the files of other days. A table that Parable creates this way is partitioned by _parable_event_date, the UTC day of the row’s event time. An older table keeps its partitioning until a rebuild. Filter on the column to read whole days:
A child table whose rows carry the root row’s event time gets the same layout, and each child row gets the day of that event time. A row with no event time has a null _parable_event_date. snapshot and changelog tables have no event layout.

Wire format

The annotation is valid only on a date-time field. An integer ordering field declares a date-time type with x-temporal-format, so that it sorts as a time.

Lineage

Both fields land as ordinary columns. GitHub copies the repository’s full_name onto each pull request, so you can filter pull requests by repository without a join.

Reader text

Reader text never changes the stored data. A change to it changes only what the catalog says (CATALOG_ONLY in When a Tap schema changes).
  • context names fields of the same row whose values go before each piece of text, such as the title.
  • text and label apply to an array of objects. text is the path to each item’s text, and label is the path to one value for each item, such as the speaker.
  • format: "adf" says that the text is an Atlassian Document Format document.
  • format: "notion" with order goes on a foreign key. Row search then reads the rows that name each referenced row, in that order, as one document.
Search explains how you search the rows.

Access

Sixteen x-access* annotations declare who can open each row in the Provider. Source permissions describes each one, with examples. An access annotation never changes how a table is planned or stored.

Workspace only

Only your Workspace’s outputSchema uses x-exclude. Taps lists the fields that you cannot exclude.

Declarations on the Tap and the plugin

These declarations are not x-* keys, but they change the tables in the same way.

On the plugin

On the Tap

A synthetic key hashes the listed fields. Google group_members keys each membership on the group and the member:
A membership that a sync reads once for each parent repeats each member’s ID once for each group. A key over the parent ID and the member ID keeps one row for each membership. Without outputField, the hash goes into the primaryKey field. A single "*" entry hashes every top-level field of the record. Some reports have columns that only your Workspace knows, such as a custom Workday report. syntheticPrimaryKey.fieldsConfigKey names a field of the connection’s ingestionConfig that lists the key columns. The columns that you list there replace fields for your Workspace. A record that lacks one of those columns fails the read.

When a Tap schema changes

When a plugin’s Tap schema changes, or your Workspace changes its outputSchema, Parable compares the new table plan with the plan that serves your queries now. Each table gets one class. A table with several changes takes the class that needs the most work. A rebuild rewrites a Tap’s stored tables from the records that Parable kept from earlier syncs. It waits for approval. Until a rebuild finishes, queries read the tables as they were before the change. Ask your Parable representative to turn on rebuild approval for your Workspace. Listing needs permission to view rebuilds. Approving and declining need permission to approve them. A rebuild names its Tap, its reasons, the tables it rebuilds (rebuildTableIds), and its state: An approval that sends an old classificationHash fails with WA-TCT-031, because the rebuild changed after you read it. Read it again and send the new hash. Approving a rebuild that a later change replaced fails with WA-TCT-032, and its details name the new rebuild. While another rebuild of the same Tap runs, an approval fails with WA-TCT-033. A rebuild that is not waiting for approval fails with WA-TCT-034. The API Reference has the full request and response shapes.