Skip to main content
Every state manager in the runtime can be seeded from source files — plain JSONL and JSON files (plus decorated Python for functions) that you keep in version control. At sync time the runtime reconciles each manager’s backing store against these files: entries you declare are inserted, entries you edit are updated, and entries you delete are removed. Content the assistant created on its own is left alone. This turns a local deployment into something you can configure like code: check your assistant’s starting contacts, guidance, secrets, tasks, and reference data into a repo, and every fresh deployment converges to the same state.

How syncing works

Each manager exposes the same pair of building blocks (in unify/<manager>/custom_*.py in the unifyai/unify repo):
  • Collectors read the source files from one or more directories and return the declared entries.
  • sync_custom() on the manager reconciles the store against those entries.
Reconciliation is hash-based and idempotent:
  1. Every source entry carries a stable key and gets a content hash. An aggregate hash over all entries is compared against the last synced value — if nothing changed, sync is a no-op, so it’s cheap to run on every startup.
  2. On a mismatch, entries are reconciled by key: new keys are inserted, changed hashes are updated in place, and keys that disappeared from the source are deleted from the store.
  3. Rows the assistant or user created through normal conversation are untouched — sync only manages rows it created. The one exception: if a manually-created row collides with a declared key, the source definition adopts it.
  4. Runtime-owned state survives updates. A recurring task keeps its execution history and status when you edit its definition, and a task that is actively running is skipped and picked up on the next sync.
  5. Invalid lines are skipped with a logged warning — one bad row never blocks the rest of the file.
Two fields are understood by every entry type: Blank lines and #-prefixed comment lines are allowed in every .jsonl file.

The override cascade

Collectors accept a list of directories and merge them in order — when the same key appears in more than one directory, the later directory wins. This gives you a layered override model:
Pass [org, me] and an entry keyed crm-runbook in me/guidance.jsonl replaces the org-level definition of the same key. The hosted product uses this same mechanism to cascade org → user → assistant configuration; a local deployment can use as many or as few layers as it wants.

Source file formats

Guidance — guidance.jsonl

One JSON object per line. function_names link the guidance to custom functions by name (resolved to ids at sync time):

Contacts — contacts.jsonl

Other supported fields: phone_number, whatsapp_number, discord_id, slack_user_id, bio, timezone. key may be omitted — it defaults to the lowercased first_name|surname pair (secrets similarly default to their name).

Secrets — secrets.jsonl

Secret values live in the file, so keep secrets source files out of shared version control (or template them in from your secret store at deploy time).

Tasks — tasks.jsonl

Declare recurring or event-triggered work. schedule and trigger are mutually exclusive; runtime status and execution metadata stay owned by the scheduler:
schedule holds an ISO-8601 start_at; repeat is a list of RFC-5545-style patterns (frequency, interval, weekdays, count, until, time_of_day); trigger names an inbound communication event (medium such as email or sms_message, optional from_contact_ids/omit_contact_ids, and recurring to re-arm after each run). Other supported fields: deadline, priority, response_policy, entrypoint_function (run a stored function instead of re-planning), offline.

Blacklist — blacklist.jsonl

Knowledge tables — directory tree

Each table is a subdirectory holding meta.json (description, column types, and the seed_key used as the per-row merge key) plus rows.jsonl. The relative path to meta.json becomes the table name, so nesting is allowed:

Reference data tables — directory tree

Same meta.json + rows.jsonl shape as knowledge, for DataManager-owned reference tables. meta.json additionally supports context (target context path), unique_keys, and auto_counting.

Dashboards — directory tree

Tiles and layouts live under tiles/ and layouts/ namespaces, each with the meta.json + rows.jsonl shape.

Functions and venvs — Python and TOML

Custom functions are ordinary Python decorated with @custom_function; venvs are pyproject.toml-style files whose filename becomes the venv name:
Files starting with _ are ignored. See the FunctionManager README for the full decorator reference.

Running a sync

Syncing is explicit — nothing watches the files. Collect from your directory layers and call sync_custom() on each manager. Sync functions first, since guidance and tasks resolve function names to ids:
Contacts, secrets, blacklist, knowledge, data, and dashboards follow the same collect-then-sync shape with their own collectors (collect_contacts_from_directories, collect_secrets_from_directories, and so on). Because every sync is hash-guarded, running the whole pass on every startup costs almost nothing when the files haven’t changed. In the hosted product this reconcile runs automatically when an assistant starts, cascading org, user, and assistant configuration layers. In a local deployment you decide when it runs — a startup script that calls the snippet above is the usual shape.