Write a sync source
A sync source mirrors records from an external system into a workspace. It is a JSON manifest: auth, the endpoints to read, and a field map from the source's records onto Cobblr fields. The generic engine polls the source and writes the rows. This page walks a complete minimal manifest and the install-and-test loop; the sync-source reference covers every section option.
The smallest thing that works
Here is a manifest that mirrors a paginated list of items from a self-hosted app into a workspace's inventory. It carries the four required pieces: identity, auth, a list endpoint, and a field map.
{
"id": "my-app",
"name": "My App",
"version": "1.0.0",
"baseUrlLabel": "My App URL",
"baseUrlPlaceholder": "http://my-app.local:8080",
"credentials": {
"token": { "label": "API key", "secret": true }
},
"auth": { "kind": "header", "header": "Authorization", "from": "token", "prefix": "Bearer " },
"test": { "method": "GET", "path": "/api/me" },
"entityTypes": [
{
"key": "items",
"label": "Items",
"targetKind": "inventory:part",
"idField": "$.id",
"list": {
"method": "GET",
"path": "/api/items",
"arrayPath": "$.items",
"paginate": { "param": "page", "sizeParam": "pageSize", "size": 100 }
},
"map": {
"name": "$.name",
"description": "$.description",
"qty": { "coalesce": ["$.quantity", "='1'"] },
"cost": "$.price"
},
"images": { "image_path": "$.photo_url" }
}
]
}
Each piece:
id/name/version: how the source is identified and shown in the connection picker.credentialsandauth: the fields the connection form asks the user for, and how they go out on the wire.prefixis prepended to the stored value, so the user pastes a raw token and the engine sendsAuthorization: Bearer <token>.test: a cheap read that resolves only when the credentials are valid, so the connection's Test button means something.entityTypes: one section per kind of record you mirror.targetKindis the Cobblr entity the rows land as (inventory:part,core-locations:location,projects:project, and so on).listgives the endpoint, where the array of records sits in the response (arrayPath), and pagination.idFieldis the record's id in the source, which the engine uses to keep an id map so re-syncs update the same rows instead of duplicating them.
The values in map are the expression language used across every declarative
integration: "$.name" reads a path, "='1'" is a literal, and coalesce takes
the first non-empty candidate. images points at a source image URL; the engine
fetches it into Cobblr's file storage and rewrites the field to the stored copy.
Records that reference each other
When one section's rows point at another's (an item that belongs to a location), mirror both sections and resolve the link through the target section's id map. This is the shape the built-in Homebox source uses, where an item's location is its parent entity:
{
"key": "items",
"targetKind": "inventory:part",
"idField": "$.id",
"list": { "method": "GET", "path": "/api/items", "arrayPath": "$.items" },
"references": {
"location_id": { "section": "locations", "from": "$.parent.id" }
},
"map": { "name": "$.name" }
}
The engine resolves $.parent.id against the locations section's id map, so the
relation survives the mirror even though the two sections import separately. Land
a section's rows in a specific instance with
targetInstance, or fan one endpoint across several instances with instanceBy.
Both are in the reference.
Keep source-only data namespaced
A source usually carries fields Cobblr has no native column for. Keep them rather
than drop them, and namespace them under the source's own key inside metadata
so they can't collide with Cobblr's own metadata keys:
"metadata": {
"object": {
"my-app": { "object": { "source": "='my-app'", "ext_id": "$.id" } }
}
}
Install and test
Paste the manifest into a new sync connection (owner or admin). The editor validates it structurally before it saves, down to warning when an instance slug doesn't match a real instance in the workspace. Then run the first import preview: it shows both sides and writes nothing until you approve, which is the fastest way to debug a field map. A path that returns nothing shows up as an empty column right there.
To ship your source as a built-in that appears in every workspace's picker rather than a pasted manifest, register it the way the Homebox and Ravelry sources are registered; see the contribution guide.