Connectors
How external services reach the box filesystem: the framework every connector shares, and one page per connector.
How connectors work
External service → Connector.sync() → Writes/reads card files → Git commit
A connector's sync() method:
- Reads its config from
_config/connectors/<name>.jsonand its credentials from the machine-level secret store (docs/secrets.md). Some box-authored integrations (capture, dropbox, raindrop, kie, omdb, tmdb) still keep their own_config/connectors/<name>.secret.json; no built-in connector does - Pulls new data from the external service
- Creates/updates card files in the box
- Stages and commits changes with structured trailers
- Optionally pushes local changes back to the service (two-way sync)
- Creates job cards for downstream processing (e.g., chat jobs for new messages)
Returns a SyncResult with { success, created, updated, pushed?, jobs?, procedures?, error? }. Procedure requests are run by wakeup orchestration after connector writes finish.
Sync rebuilds a connector-managed card's content wholesale from its template; any agent-added field the template doesn't know about is lost unless it's one of the few fields preserve-agent-fields.ts explicitly carries forward (currently just contains).
Members
Every built-in connector is service-injected (see below). Shared Google authorization for the three Google connectors: Google auth.
| Connector | File | Card types | Direction | Page |
|---|---|---|---|---|
| Google Calendar | google-calendar.ts | .ics files | Two-way | Calendar |
| Gmail | gmail.ts | email-thread, email-message, email-outbound | Two-way (pull + draft upload) | Gmail |
| Google Drive | google-drive.ts | gsheet, gdoc, gfolder, glink | Two-way | Drive |
Lifecycle
Connectors are called during bbx wakeup:
wakeup.tsloads connector configs for the box- Creates connector instances (with optional service injection)
- Calls
syncConnector(connector, { boxRoot })on each, which runssync()and records the attempt (see Activity record below) - Reports results
Every caller that syncs a connector (wakeup, bbx finalize, the sync command, bbx drive sync) goes through syncConnector, never sync() directly, so each attempt is recorded once.
Activity record
connectors/activity.ts keeps, per connector and box-local day, the number of sync attempts and how many succeeded, errored or skipped, plus the counts of new items, raw created paths and updated paths. It lives in machine-local transient state (_bookkeeping/connectors/connector-activity.state.json) and keeps 60 days.
A new item is a created path outside any .attach/ scope, that is, a new top-level card. Files inside a card's attachment scope belong to a card that already counted: Gmail lists every new message file in created, including replies on threads the box already tracks, so raw created would count a connector that only refreshes old threads as productive.
A connector reports what it did through SyncResult as before. To be counted correctly it must put new top-level cards in created, and set error when part of the sync failed even if the rest succeeded (Gmail does this for a draft that fails to upload).
connectors/activity-verdict.ts reads the record and decides whether a connector has gone quiet or keeps failing; the scheduler records each episode once, notifies the boxholder once for a failing one, and the dashboard shows a warning. See health-checks.md.
Configuration
A connector with non-credential config reads it from _config/connectors/<name>.json; the shape, and whether one exists at all, is on the connector's page (Drive's configuration is its cards). Credentials come from the machine secret store. Transient state (sync cursors, mappings) lives in _bookkeeping/connectors/<name>.state.json or <name>-state.json, machine-owned and gitignored.
Freshness metadata
A convention for any file that caches an external source of truth, applied wherever a cache is designed.
Any file that's a cache of an external source of truth (Gmail/Calendar/Drive snapshots, web fetches, synthesized briefings) should carry freshness metadata, but only where staleness would cause confidently-wrong output. Recipe categories: don't bother. Calendar snapshots: definitely.
Two timestamps, not one: last_sync (when we last checked) and data_through (cutoff of the actual data). They diverge when a check found nothing new — without both, "May 11" is ambiguous between "stopped checking" and "checked, nothing new." Sidecar <file>.sync.json is usually cleaner than an inline header (no diff noise in the human-readable file).
Hardest case: synthesized caches. A briefing built from 30 cards has data_through = min(inputs.data_through), not its own generation timestamp. If the pipeline doesn't propagate this, the briefing looks fresh while resting on stale inputs — this is where silent-stale bugs actually live.
When and how an agent tells the user about staleness is a prompt lens.
Service injection
Connectors that call external APIs accept an optional service parameter:
export function createGmailConnector( boxRoot: string, service?: GoogleGmailService, ): Connector
When no service is provided, connectors create real implementations from config files. When a fake is injected (in tests), all API calls go through the fake.
See src/services/AGENTS.md for the full service layer documentation.
Writing a new connector
- Implement the
Connectorinterface (name,sync()) - Declare and resolve credentials via the machine secret store (
docs/secrets.md) rather than a new<name>.secret.jsonfile - Use transient state for sync cursors/offsets
- Stage and commit all file changes with descriptive messages and trailers
- Create job cards when new items need processing
- Register via
registerConnector()in the connector registry - Add a service interface if the connector calls external APIs (see
src/services/AGENTS.md)
Shared utilities
chat-utils.ts— Thread file management:ensureThreadFile(),appendMessageToThread(),findUnsentAgentMessages(),stampSentMessage(),safeFilename(),updatePersonEntry()calendar-utils.ts— ICS parsing, event formatting, timespan parsingintake-utils.ts—createOrAppendIntakeJob()for creating reactor inbox-processing jobs (legacy reactor path, distinct from the new intake → triage → handle pipeline indocs/triage.md)transient-state.ts—loadTransientState()/saveTransientState()for non-committed statecalendar-config.ts— Calendar sync configuration management