Skip to content

Integrations

Purpose

This doc is the contract every connector must satisfy to plug an external system into the Harness and the Org Model: what a connector may read, write, and subscribe to; how data is backfilled and kept fresh; how each external object maps into Org Model nodes and evidence records; who owns each field when two systems disagree; behavior under rate limits; what managers may still do when a connector is down; how connector health is scored; and the checklist a new connector must clear before a tenant can install it. The PRD's stance is "five connectors done completely beat thirty done badly." This doc is what "done completely" means.

Scope / Non-scope

In scope

  • The connector manifest and runtime contract (capabilities, scopes, auth, sync, health, degradation).
  • Sync model: initial backfill, incremental sync, webhooks, polling fallback, reconciliation.
  • Canonical mappings for the v1 connector set: Slack, Microsoft Teams, Google Workspace (Calendar, Gmail-as-manager-mailbox, Drive), Linear / Jira, GitHub, Gusto / Rippling / Deel, QuickBooks / Xero / Ramp, Square / Toast, Deputy / 7shifts, Twilio SMS and voice.
  • Write-back rules, source-of-record per field, conflict resolution.
  • Rate limits, backoff, priority classes, per-tenant API budgets.
  • Degradation modes and connector health scoring.
  • Edge cases (EC-INT-*), risk-control loops, and the new-connector checklist.

Out of scope

  • The Org Model schema (02-org-model-spec.md) and its API (17-data-model-and-api-reference.md).
  • Policy evaluation of the actions writes trigger (05-policy-and-rule-engine.md); this doc says which writes exist and their default class.
  • Atoms solver behavior and SMS/voice conversation design (12-atoms-mode-spec.md, 10-communication-engine-rules.md).
  • Tenant-level security beyond connector scope (13-security-privacy-compliance.md).
  • Asana, Notion, BambooHR, Workday, Brex, Stripe, Shopify POS, When I Work, Homebase, IoT feeds: same contract, mappings added per §10.

1. Connector contract

A connector is a versioned, tenant-instantiated adapter that runs in its own sandboxed process, holds exactly one tenant's credentials, and speaks to the Harness only through the event bus and the Org Model proposal API. Connectors never talk to a model. Connectors never talk to each other.

1.1 Capabilities

Capability Meaning Granted when
read Pull entities from the external system into evidence records and Org Model proposals. Tenant installs the connector. Always required.
subscribe Receive push events (webhooks, event subscriptions, streaming APIs). Treated as a hint to re-read, never as truth. Installed and the external system supports it. Falls back to polling (§2.4).
write Mutate the external system: post a message, comment on an issue, create a calendar event, submit a PTO request, publish a schedule. Every write is an Action that passed the policy engine. Only after the owning function has graduated to PROPOSE or above for that tenant (16-rollout-and-operations.md). Write scopes are requested lazily at that moment, not at install.

1.2 Manifest

Every connector ships a manifest. The Harness validates it at build time and re-validates granted scopes against it at install and every 24 hours.

type SystemFamily = "comms" | "work" | "people" | "finance" | "atoms" | "identity";
type Capability = "read" | "write" | "subscribe";
type ScopeTier = "core" | `feature:${string}` | "never";

type ScopeRequest = {
  scope: string;              // vendor scope string, e.g. "channels:history"
  tier: ScopeTier;            // core = needed to run; feature = granted when feature enabled; never = documented refusal
  capability: Capability;     // which capability this scope serves
  justification: string;      // rendered verbatim on the consent screen and the "what my manager knows" page
  dataClassMax: "public" | "internal" | "personal" | "sensitive-personal"; // highest class this scope can surface
};

type EntityMapping = {
  external: string;           // "slack.message", "linear.issue"
  target: "orgmodel" | "evidence";
  type: string;               // Org Model node type or evidence record type
  keyFields: string[];        // fields forming the idempotency key
  dropFields: string[];       // fields the connector must strip before emitting (see 13-security-privacy-compliance.md §3)
  classifier?: string;        // name of a content classifier to run on free-text fields
};

type FieldAuthority = {
  field: string;              // "Person.employment_status"
  sourceOfRecord: "external" | "seasuite";
  writeClass?: "AUTONOMOUS" | "PROPOSE_CONFIRM" | "HUMAN_ONLY" | "NEVER";
};

type ConnectorManifest = {
  id: string;                 // "slack" | "linear" | "gusto" ...
  version: string;            // semver
  system: SystemFamily;
  capabilities: Capability[];
  auth: "oauth2" | "app_install" | "bot_token" | "api_key";
  scopes: ScopeRequest[];
  entities: EntityMapping[];
  authority: FieldAuthority[];
  sync: {
    backfill: { order: string[]; windows: Record<string, string>; chunk: number };
    incremental: { strategy: "cursor" | "updated_since" | "changelog"; interval: string };
    subscribe?: { transport: "webhook" | "events_api" | "pubsub"; expectedEventGap: string };
    pollFallback: { interval: string; reconcileDaily: true };
  };
  rateLimit: { defaultBudgetPerMin: number; headers: string[]; retryAfterHeader?: string };
  health: { freshnessSlo: string; errorRateSlo: number };
  degradation: DegradationProfile;   // §6
};

1.3 Scopes and least privilege

Rules, enforced in the Harness:

Rule Statement
R-SCOPE-1 A connector may only use scopes in its manifest. Granted scopes are diffed against the manifest at install and daily; any extra scope is scope drift and the connector pauses until an admin re-consents (EC-INT-009).
R-SCOPE-2 core scopes are requested at install; feature:* scopes when the tenant enables the feature, with the feature named on the consent screen.
R-SCOPE-3 never scopes are documented with the reason and rendered on the "what my manager knows" page as "this manager cannot read X" (human-to-human DMs, HRIS compensation endpoints, any camera/keystroke/location scope).
R-SCOPE-4 Write scopes are never requested at install (§1.1).
R-SCOPE-5 A scope whose dataClassMax is sensitive-personal is usable only for HUMAN_ONLY packet preparation, and only for explicitly allowed fields.
R-SCOPE-6 Tokens live in the tenant vault (envelope-encrypted with the tenant KEK) and are injected at process start. They never appear in the event bus, evidence, receipts, logs, or model context.

Worked example, Slack:

Tier Scopes Justification shown to tenant
core users:read, users:read.email, channels:read, team:read, im:write, im:history (bot's own DMs only), chat:write, app_mentions:read Find the people on the team, open a manager thread with each person, read replies in that thread.
feature:channel-reading channels:history (only channels the bot is a member of), groups:history (only private channels the bot is explicitly invited to), reactions:read Read work conversation in channels where the manager has been added, so it can notice blockers without asking.
feature:calendar-bridge users:read (tz field) Deliver briefs in your local morning.
never mpim:history and im:history for human-to-human DMs, files:read on private files, admin.*, dnd:read presence tracking The manager does not read private messages between people, does not track presence, and has no admin power over the workspace.

1.4 Runtime isolation

One process per (tenant, connector), with CPU/memory caps and an egress allowlist limited to the vendor's API hosts. Outbound interfaces to Sea Suite: emitEvidence, proposeOrgChange, emitEvent, reportHealth. Inbound: executeWrite with a signed Action envelope carrying the policy decision id. Connectors are stateless beyond a checkpoint store keyed (tenant, connector, entity).

2. Sync model

2.1 Connector lifecycle

stateDiagram-v2
    [*] --> UNINSTALLED
    UNINSTALLED --> AUTHORIZING: tenant starts install
    AUTHORIZING --> BACKFILLING: scopes validated
    AUTHORIZING --> UNINSTALLED: consent denied / scope mismatch
    BACKFILLING --> LIVE: backfill complete, freshness within SLO
    LIVE --> DEGRADED: health below 70 or freshness breach
    DEGRADED --> LIVE: health 90 or above for 15 min
    DEGRADED --> PAUSED: health below 40 or auth error x3
    LIVE --> PAUSED: admin pause / scope drift / kill switch
    PAUSED --> AUTHORIZING: re-consent required
    PAUSED --> LIVE: admin resume (no re-consent needed)
    PAUSED --> REVOKED: token revoked upstream / tenant uninstall
    LIVE --> REVOKED: token revoked upstream
    REVOKED --> [*]

Transitions emit connector.state_changed events with reason and actor; they are visible on the tenant health dashboard (15-observability-and-runbooks.md).

2.2 Initial backfill

Backfill runs during onboarding and the listening period (16-rollout-and-operations.md). It is chunked, resumable, and rate-budgeted so it cannot starve interactive traffic.

Order is fixed: identity → structure → activity. People before channels before messages; users before projects before issues. This guarantees every activity record can be linked to a Person on emission rather than parked as an orphan.

Default windows (tenant-overridable, never wider than the connector's dataClassMax allows):

Entity family Window Rationale
People, roles, orgs All current + terminated in last 90 days Attribution of recent work; retention applies after backfill.
Channels / teams / projects All Structure is small.
Work items (issues, PRs) All open + closed in last 180 days Velocity baselines need two quarters.
Messages in permitted channels 90 days Enough for cadence discovery.
Calendar 60 days back, 90 forward Cadence discovery, meeting baselines.
Finance transactions Fiscal year to date + prior 12 months Budget-vs-plan needs a trailing year.
POS / shifts / time entries 26 weeks Demand forecasting needs seasonality.

Checkpoint schema:

type SyncCheckpoint = {
  tenantId: string;
  connectorId: string;
  entity: string;                 // "linear.issue"
  phase: "backfill" | "incremental" | "reconcile";
  cursor: string | null;          // vendor cursor or ISO timestamp watermark
  chunkIndex: number;
  recordsSeen: number;
  lastSuccessAt: string;          // ISO
  lastAttemptAt: string;
  consecutiveErrors: number;
  etagCache?: Record<string, string>; // externalId -> version/etag for write CAS
};

2.3 Incremental sync and webhooks

sequenceDiagram
    participant V as Vendor API
    participant W as Webhook Receiver
    participant C as Connector (tenant sandbox)
    participant B as Event Bus
    participant OM as Org Model / Evidence Store
    participant H as Harness

    V->>W: POST webhook (signed)
    W->>W: verify signature, dedupe on event id (24h window)
    W-->>V: 200 within 1s
    W->>C: enqueue hint with entity, externalId, eventType
    C->>V: GET entity by id (truth is the API, not the payload)
    V-->>C: entity + etag
    C->>C: strip dropFields, run classifier, map to canonical
    C->>OM: upsert evidence / propose org change (idempotent on key)
    C->>B: emit connector.entity_changed
    B->>H: trigger subscribed manager runs (debounced 30s per entity)

Rules:

Rule Statement
R-SYNC-1 Webhook as notification, API as truth. The connector never stores payload contents; it re-reads the entity. This kills spoofed-content and stale-payload bugs in one move.
R-SYNC-2 Every upsert is idempotent on (tenantId, connectorId, entity, externalId). Re-delivery is harmless.
R-SYNC-3 The webhook receiver acks in under 1s and does no work; verification failures are counted (RB-13) and never processed.
R-SYNC-4 Incremental sync runs on its interval regardless of webhooks, with a 5-minute updated_since overlap.
R-SYNC-5 Connector-triggered manager runs are debounced per entity (30s) and per person (5 min): a burst of edits produces one run.
R-SYNC-6 Deletes are found by the daily reconcile sweep, because most vendors do not reliably webhook deletes. A missing entity is tombstoned, never removed; references from Goals/Projects are flagged (EC-INT-005).

2.4 Polling fallback

The connector switches from subscribe to poll when any of the following hold, and switches back when the condition clears for 30 minutes:

  • Webhook subscription creation fails or the vendor reports it inactive.
  • No webhook received for 3 × expectedEventGap while the incremental sync shows changes (i.e., the system is active but push is silent).
  • Signature verification failure rate over 5% in a 10-minute window (possible key rotation).

Polling interval defaults: comms 2 min, work 5 min, people 30 min, finance 30 min, atoms POS 5 min, scheduling 5 min. Polling consumes the same per-tenant rate budget (§5) at backfill priority, so it can never starve an IC reply.

3. Canonical mappings into the Org Model

Connectors produce two kinds of output:

  1. Org Model proposals — candidate Person, Role, Org, Project, Goal, Cadence nodes and edges. During onboarding these are drafted for the owner to correct (OM-2). After onboarding, structural proposals (a new person, a new project) are PROPOSE_CONFIRM unless the tenant has enabled auto-accept for that connector's identity family (people from the HRIS are auto-accepted; people from Slack are not).
  2. Evidence records — typed, immutable facts linked to Org Model nodes: WorkItem, PullRequest, Message, Thread, CalendarEvent, Document, TimeEntry, Shift, PtoRequest, Transaction, Expense, Invoice, Sale, LaborSummary, Call, Sms. Receipts cite evidence records by id. Evidence never carries model rationale; it is what the rationale points at.

Every record carries {tenantId, connectorId, externalId, externalUrl, observedAt, sourceVersion, dataClass}.

3.1 Comms: Slack and Microsoft Teams

External object Target Key fields Notes
user / member Person proposal email, display name, tz, title, is_bot, deleted Identity resolution §3.7. Bots become Person{kind:"agent"} only if the tenant registers them as managed agents (AG-1); otherwise dropped.
channel / team channel Org hint (not a node) + Thread container name, members, purpose Channel membership is a signal for org membership, never authoritative. Channel purpose text feeds cadence discovery.
message in a permitted channel Message evidence author, ts, text, thread_ts, mentions, links, reactions Text is classified before store; sensitive-personal hits are redacted at ingestion with a redacted:true marker. Edits produce a new version; citations pin the version the manager read.
message in the manager's own DM thread Message evidence, Thread{kind:"manager"} as above The only DMs the connector can read.
scheduled message / reminder — — Not ingested.
user presence, DND, typing — — never scope. Not requested.
Teams meeting transcript — — Not ingested in v1; requires explicit per-meeting opt-in design (open question).

Cadence discovery: recurring posts (standup, weekly) with a stable weekday/hour in a channel over 4+ weeks produce a Cadence proposal with discovered_from: [Message ids].

3.2 Google Workspace (Calendar, Gmail, Drive)

External object Target Key fields Notes
Calendar event on a person's primary calendar CalendarEvent evidence start/end, attendees (emails), title, recurrence, organizer, visibility Private events: free/busy only (title and attendees dropped). Recurring events with >3 internal attendees become Cadence proposals.
Free/busy CalendarEvent{kind:"busy"} start/end Used by meeting-by-exception and quiet-hour inference.
Gmail (manager's own mailbox) Message evidence from, to, subject, body, thread Only the manager's mailbox (manager@tenant.seasuite.app), used for vendor chasing and inbound requests. Employee inboxes are never read.
Drive document shared with the manager Document evidence id, title, owner, modified, mime Metadata + text only when the manager account has explicit access. Used for plan/goal docs.
Directory user (Workspace Admin SDK) Person proposal email, name, org unit, manager read only; org unit is an Org hint.

3.3 Work: Linear / Jira and GitHub

External object Target Key fields Notes
Linear team / Jira project Org hint + Project proposal key, name, lead, members A Linear team maps to an engineering Org if it matches HRIS structure; otherwise to a Project. Owner corrects during onboarding.
Linear project / Jira epic Project proposal name, lead, target date, status, progress Goals are never auto-created from work tools; a project may link to a Goal.
Issue WorkItem evidence id, title, state, assignee, priority, estimate, cycle/sprint, created/updated, blocked_by, labels Titles and descriptions are classified and injection-scanned (14-evaluation-and-testing.md §5). Cross-team moves keep the same externalId (EC-INT-007).
Cycle / sprint Cadence{kind:"planning"} proposal start/end, team Cycle length feeds capacity model.
Comment Message{channel:"issue"} author, body, ts Used for stall detection ("last human activity").
GitHub repository Project hint name, teams Only repos the app is installed on.
Pull request PullRequest evidence number, author, reviewers, state, opened/merged, review_requested_at, last_activity, draft, size Bot-authored PRs (dependabot, renovate) are tagged author.kind="bot" and excluded from stall detection; they are aggregated weekly instead.
Review / review request Message{channel:"review"} reviewer, state, ts The 20-hour unreviewed PR rule lives in 05-policy-and-rule-engine.md; this connector just emits the facts.
Check run / deployment WorkItem{kind:"ci"} status, conclusion Failure streaks feed Chief Build Officer's incident cadence.
Commit message — — Not ingested as text (injection surface, low signal); commit counts only.

3.4 People: Gusto / Rippling / Deel

External object Target Key fields Notes
Employee / contractor Person (auto-accepted, HRIS is identity SoR) work email, legal name → Person.legal_name (personal class), preferred name, start date, employment status, employment type, work location, department, manager Dropped at ingestion: compensation, bank details, SSN/tax ids, date of birth, home address, demographic/EEO fields, medical/leave reasons, background-check status (EC-INT-010). The connector's drop list is tested in CI against recorded fixtures.
Department / team Org proposal name, parent, head HRIS structure is the default org tree; owner corrects.
Job title Role proposal title, level Level is stored; it is never used as a seniority signal in the Org Model, only as function.
PTO policy Policy{kind:"pto"} accrual, caps Read only.
PTO balance Person.pto_balance (personal) hours by type Visible to the person and the Chief Human Officer for quiet HR.
PTO request PtoRequest evidence dates, type, status, approver Sea Suite may create requests on the IC's explicit ask; approval state is owned by the HRIS.
Leave of absence PtoRequest{kind:"leave"} with reason dropped dates only Reason is sensitive-personal; only "on leave until X" enters the model.
Termination / offboarding event Person.employment_status change effective date Triggers EC-INT-003-style checks and the retention clock. The connector never writes this field (HUMAN_ONLY in the HRIS itself).

3.5 Finance: QuickBooks / Xero / Ramp

External object Target Key fields Notes
Chart of accounts / classes / departments Org mapping table account, class → Org Owner maps classes to Orgs during onboarding; unmapped spend lands in an "unallocated" bucket the Chief Fin Officer chases.
Transaction / journal line Transaction evidence date, amount, account, class, vendor, memo Memo text is classified and injection-scanned. Card numbers never ingested.
Bill / invoice Invoice evidence vendor, amount, due, status Feeds cash/runway reporting and vendor chasing.
Ramp card transaction Expense evidence cardholder → Person, amount, merchant, category, memo, receipt_present, policy_flags Receipt images are not ingested; only receipt_present.
Ramp reimbursement / approval request Expense{status:"pending"} approver chain In-policy approvals are the Chief Fin Officer's AUTONOMOUS write (§4.2); over-threshold routes to the approvals inbox.
Budget (if the vendor has one) — — The budget plan lives in the Sea Suite budget ledger; vendor budgets are ignored to avoid two sources of record.

3.6 Atoms: Square / Toast, Deputy / 7shifts, Twilio

External object Target Key fields Notes
Location (POS or scheduling) Org{kind:"location"} name, address, tz, hours Location tz governs quiet hours and shift math (EC-INT-006).
Order / sale (Square, Toast) Sale evidence, aggregated to 15-min buckets ts, gross, net, item count, channel Customer PII is never ingested; item-level data only as aggregates. Free-text item modifiers are not ingested (injection surface).
Labor summary (Toast) / timecard (Square) LaborSummary, TimeEntry person, clock in/out, breaks, role Break compliance evaluation lives in the scheduling solver; the connector emits raw entries.
Employee (POS) Person proposal name, email/phone, role Lower trust than HRIS; merged by phone/email in §3.7.
Shift (Deputy, 7shifts) Shift evidence person, location, start/end, role, status (published/open/swapped), created_by If the tenant designates the Atoms scheduling solver as SoR, Deputy/7shifts becomes the publishing surface (§4.1).
Availability / unavailability Person.availability (personal) windows
Shift swap / drop request Shift{kind:"swap_request"} requester, taker, status Swaps within labor rules are AUTONOMOUS after graduation (PRD §11).
Time-off (scheduling tool) PtoRequest dates If an HRIS is connected, HRIS wins; the scheduling tool mirrors it.
Twilio inbound SMS Sms evidence + manager thread message from (E.164 → Person), body, ts Unknown numbers are quarantined; no Person, no manager run (EC-INT-012).
Twilio outbound SMS status delivery receipt on the outbound Message status, error code Undelivered briefs count against the brief-delivery SLO.
Twilio voice call Call evidence from, to, duration, recording_ref (only if tenant enables and both-party consent handled) Transcript is personal; stored only with the voice line feature enabled.

3.7 Identity resolution

Persons arrive from up to six systems. Resolution order: (1) the HRIS record is the anchor, matched by case-folded work email; (2) a previously linked external id in Person.identities[]; (3) work email from comms/work tools; (4) E.164 phone (atoms); (5) a name match within the same Org produces a PROPOSE_CONFIRM merge proposal, never an auto-merge (EC-INT-004).

type Identity = { connectorId: string; externalId: string; email?: string; phone?: string; verifiedAt: string };
// Person.identities: Identity[] — each link records how it was verified.

Unmatched users after backfill are listed for the owner as "people the manager can see but doesn't know" and are not managed until resolved (EC-INT-003).

4. Write-back rules and conflict resolution

4.1 Source of record per field

Sea Suite is the source of record only for management-layer objects. Everything else belongs to the external system; Sea Suite caches, cites, and writes through the connector.

Field / object Source of record Sea Suite may write? Default write class
Person.legal_name, employment_status, employment_type, start_date, work_location HRIS No NEVER (HRIS fields that are HUMAN_ONLY by nature)
Person.display_name, pronouns, tz, preferred_channel, brief_window, quiet_hours Sea Suite (set by the person) Yes (person's own thread) AUTONOMOUS on the person's explicit ask
Person.manager_of_record, Person.managed_by (Chonk) Sea Suite Yes PROPOSE_CONFIRM (owner approves)
Person.pto_balance HRIS No —
PtoRequest (create) HRIS holds state; Sea Suite creates Yes AUTONOMOUS on the IC's explicit ask; approval state is read back
Org, Role structure HRIS (draft) → Sea Suite (approved Org Model) Yes, in Org Model only PROPOSE_CONFIRM
Goal, Plan, Cadence, Policy, Decision, feedback, growth plan Sea Suite Yes Per 05-policy-and-rule-engine.md
WorkItem.state, assignee, priority, estimate Work tool Yes PROPOSE_CONFIRM (assignee/priority), AUTONOMOUS for comments and labels after graduation
WorkItem comment Work tool Yes AUTONOMOUS after graduation; subject to outreach cap
PullRequest review request GitHub Yes AUTONOMOUS after graduation (chasing a reviewer = requesting review + message)
CalendarEvent (create) Google/Outlook Yes AUTONOMOUS with meeting-by-exception rule (IC-5): invite must state the written attempt and reason
Slack/Teams message in manager thread Sea Suite authored, vendor stored Yes AUTONOMOUS (subject to outreach cap, quiet hours)
Slack/Teams message in a channel as above Yes AUTONOMOUS for ritual write-ups after graduation; PROPOSE_CONFIRM for anything naming a person's performance
Expense approval Ramp Yes AUTONOMOUS within policy; PROPOSE_CONFIRM over threshold
Transaction, Invoice (create/modify) Accounting No NEVER in v1 (the Chief Fin Officer reports; it does not book)
Budget plan / envelope Sea Suite budget ledger Yes PROPOSE_CONFIRM for envelope changes
Shift publish, swap approval Scheduling tool unless tenant sets atoms.solverIsSoR=true Yes PROPOSE_CONFIRM until Atoms scheduling graduates; swap within labor rules AUTONOMOUS after
TimeEntry edits Time clock No NEVER (wage/hour is HUMAN_ONLY)
Outbound SMS / call Twilio Yes AUTONOMOUS within caps and quiet hours; voice outbound PROPOSE_CONFIRM until the voice line graduates

4.2 Write execution contract

Every write is an Action that has already passed the policy engine. The connector receives a signed envelope and performs a compare-and-set.

type WriteEnvelope = {
  actionId: string;
  policyDecisionId: string;        // proves the policy engine approved this action
  autonomyClass: "AUTONOMOUS" | "PROPOSE_CONFIRM";  // HUMAN_ONLY never reaches a connector
  confirmedBy?: { personId: string; at: string };   // required when PROPOSE_CONFIRM
  target: { entity: string; externalId: string; expectedVersion?: string };
  op: "create" | "update" | "comment" | "send";
  payload: Record<string, unknown>;
  idempotencyKey: string;          // hash(actionId) — vendors that support it get it as a header
  receiptsRef: string;
  deadline: string;                // ISO; the write is abandoned (not retried) after this
};
executeWrite(env):
  assert verifySignature(env) and env.autonomyClass != HUMAN_ONLY
  if env.autonomyClass == PROPOSE_CONFIRM: assert env.confirmedBy
  if alreadyExecuted(env.idempotencyKey): return priorResult      # exactly-once at our layer
  if env.target.expectedVersion:
      current = api.get(env.target)
      if current.version != env.target.expectedVersion:
          return conflict(current)                                # §4.3
  result = api.write(env.op, env.payload, headers={Idempotency-Key: env.idempotencyKey})
  recordExecution(env.idempotencyKey, result)
  emit audit("connector.write", env, result)
  return result

4.3 Conflict resolution

Conflicts happen when a human changed the target between the manager's read and its write. Rule: never overwrite a human edit without re-evaluating. Last-writer-wins is forbidden for any field a human can edit.

onConflict(env, current):
  diff = fieldsChanged(env.target.snapshot, current)
  if disjoint(diff, env.payload.fields):
      # human changed other fields; our change is still valid
      return retryWithVersion(env, current.version)          # max 1 retry
  if env.autonomyClass == AUTONOMOUS and isBenignSupersede(diff, env):
      # e.g. we wanted to comment "unblocked?" and the human already closed the issue
      return abandon(env, reason="superseded_by_human")        # logged; no message sent
  # the human moved the thing we were about to move
  return escalate(env, to="proposal", receipts += [current], reason="conflict_with_human_edit")

isBenignSupersede is a small, tested table: closed/merged/cancelled states supersede chases; a human-assigned reviewer supersedes a review request; a human-published schedule supersedes a pending solver proposal (EC-INT-013). Anything else becomes a PROPOSE_CONFIRM proposal with both versions in the receipts.

5. Rate limits and backoff

5.1 Budgets

Vendor limits change; the connector treats the manifest's defaultBudgetPerMin as a starting budget and adapts from response headers (X-RateLimit-Remaining, Retry-After, GraphQL complexity cost) on every call. The numbers below are budgeting defaults, to be verified against vendor docs at build time.

Connector Default budget Adaptive signal Notes
Slack 50 req/min per method tier; chat.postMessage ~1 msg/s/channel Retry-After on 429 Sends are the scarce resource; briefs are pre-scheduled to spread.
Teams (Graph) ~4 req/s per app per tenant Retry-After Throttled per resource type.
Google Workspace Per-project daily quota + per-user per-second 403 rateLimitExceeded, 429 Backfill fans out across users.
Linear ~1,500 req/hr per key, complexity-weighted remaining + complexity headers One GraphQL query per team per sync.
Jira Cloud ~100 req/min safe 429 + Retry-After JQL updated >= searches.
GitHub App 5,000 req/hr per installation; secondary limits on mutations X-RateLimit-*, 403 Mutations spaced ≥ 1 s.
Gusto / Rippling / Deel Tens/min 429 30-min incremental is enough.
QuickBooks / Xero ~500/min (QBO); 60/min + daily cap (Xero) 429, Xero problem header Xero's daily cap forces nightly reconcile.
Ramp ~100 req/min 429
Square / Toast / Deputy / 7shifts Per-endpoint, low 429 Reads once per 5 min per location.
Twilio Queue per number; long codes ~1 msg/s; 10DLC by campaign tier 429, queue length Registered 10DLC campaign required before US GA.

Per-tenant budget is min(vendorBudget, fairShare), so one tenant's backfill cannot consume a shared app-level quota (GitHub App, Slack app).

5.2 Priority classes

Requests are queued per (tenant, connector) with four priorities. Higher priority preempts lower; lower priorities are the ones that back off first.

Priority Examples Starvation guard
P0 interactive Reading a message the IC just sent; writing the reply Reserved 20% of budget.
P1 scheduled outreach Briefs, cadence write-ups, in-policy approvals Reserved 30%.
P2 detection Incremental sync, stall detection reads, chases Remainder.
P3 bulk Backfill, reconcile, reprocessing Only idle budget; paused entirely when health < 70.

5.3 Backoff

retry(call, priority):
  attempt = 0
  while attempt < maxAttempts[priority]:         # P0: 3, P1: 5, P2: 6, P3: 8
      resp = call()
      if resp.ok: return resp
      if resp.status == 429 or resp.status in (500,502,503,504):
          wait = resp.retryAfter or min(cap, base * 2**attempt) * random(0,1)   # full jitter
          if resp.status == 429: budget.shrink(0.5)  # halve budget until a clean 15-min window
          sleep(wait); attempt += 1; continue
      if resp.status in (401,403): return authFailure(resp)   # no retry; health event
      return resp
  breaker.trip(30s * 2**breaker.trips)            # circuit breaker per connector
  return exhausted

Base 1s, cap 5 min. A tripped breaker moves the connector to DEGRADED and triggers the degradation profile (§6). Writes with a passed deadline are abandoned, not retried; the Harness decides whether to re-plan.

6. Degradation modes

Principle: stale-labeled, never silent. When a connector is degraded, managers keep working from the last good snapshot, but every receipt and every user-facing output built on stale data carries a staleness marker, and actions whose safety depends on freshness are downgraded one autonomy class or suspended.

type DegradationProfile = {
  freshnessSlo: string;        // "15m" for comms, "1h" for work, "24h" for people/finance
  staleActions: Array<{ action: string; when: "stale" | "down"; effect: "downgrade" | "suspend" | "reroute" }>;
  fallbackSurface?: string[];  // for comms: ["email", "sms", "app_push"]
};
Connector down Managers may still Managers must not Fallback / what the manager says
Slack / Teams Read snapshot; compose briefs and replies; queue outbound with 4h TTL Send via the dead surface; treat silence as a non-reply (stall clocks pause) Reroute briefs to the person's fallback surface if configured. Thread shows "Slack is unreachable; I'm on email until it's back."
Linear / Jira / GitHub Plan from a snapshot ≤ 24h old, labeled; answer questions with a staleness note Chase anyone on stall math (stall detection pauses; false chases are the trust-cliff risk) Health view shows "work data as of HH:MM". Stall clocks resume on recovery; they never backfill.
Google Calendar Use free/busy snapshot ≤ 24h Book meetings (suspend) Manager proposes times in writing instead.
HRIS Use identity snapshot; answer policy questions from cached Policy Create PTO requests, state balances, onboard/offboard (suspend) Quiet HR requests are acknowledged and queued with a promise of confirmation.
Accounting Report from a ledger snapshot ≤ 24h, labeled Present cash/runway as current; runway math suspended past 48h Chief Fin Officer's update leads with the staleness.
Ramp Approve within policy from a snapshot ≤ 6h Approve past 6h (downgrade to PROPOSE_CONFIRM) Approvals queue with a note to the requester.
POS Use last forecast; run huddles Re-forecast; propose sales-driven schedule changes (suspend) Location health shows "sales data as of…".
Scheduling Serve the cached published schedule by SMS; take swap/call-out requests Publish or modify shifts; confirm a swap as final (suspend) Swaps recorded as pending_replay, replayed with CAS on recovery; the IC is told "tentative until the schedule syncs."
Twilio Deliver via app push / email where present Assume SMS-only staff received anything Manager of record notified within 15 min that deskless staff are unreachable.
HRIS down > 72h Continue with cached identities Auto-accept new people from any source New people wait as proposals.

Recovery rule (R-DEG-1): on return to LIVE, the connector runs a reconcile before any manager run consumes the data; queued outbound messages older than their TTL are dropped and logged, not sent (nobody wants Tuesday's brief on Thursday).

7. Connector health scoring

Health is a 0–100 score computed per (tenant, connector) every minute from the last 15 minutes of samples.

health = 0.40 * freshness + 0.25 * reliability + 0.15 * auth + 0.10 * push + 0.10 * scope

freshness   = clamp(1 - (ageOfNewestSuccessfulSync / freshnessSlo), 0, 1)
reliability = 1 - errorRate            # non-2xx excluding 429 that recovered within budget
auth        = 1 if last auth check ok else 0
push        = 1 if subscribe healthy or not applicable; 0.5 if on poll fallback; 0 if both failing
scope       = 1 if grantedScopes ⊆ manifestScopes and coreScopes ⊆ grantedScopes else 0
Score State Effect
≥ 90 LIVE Normal.
70–89 LIVE (warning) Dashboard warning; P3 traffic throttled 50%.
40–69 DEGRADED Degradation profile applies; alert SEV3; P3 paused.
< 40, or auth = 0 for 3 checks, or scope = 0 PAUSED All writes suspended; alert SEV2; tenant admin notified with the fix.

Scores are per tenant, so one tenant's revoked token never affects another; fleet medians per connector detect vendor-side outages (RB-01).

8. Edge cases

ID Scenario Signal / detection Rule Action (autonomy class) Escalation Test
EC-INT-001 Vendor redelivers a webhook 3 times over 2 hours. Same event id within 24h dedupe window. R-SYNC-2, R-SYNC-3. Drop duplicates; single re-read (AUTONOMOUS). None. Sim org chonk-test-co, FakeLinear.redeliver(eventId, n=3); assert one upsert, zero duplicate manager runs.
EC-INT-002 Slack admin revokes the bot token during a brief send. 401 on chat.postMessage; auth check fails. No retry on 401; connector → PAUSED. Remaining briefs reroute to fallback surface if configured, else queued 4h (AUTONOMOUS). Tenant admin notified with re-consent link; manager of record notified if > 50% of ICs unreachable. FakeSlack.revokeToken() mid-brief.run; assert no partial sends repeated after re-auth.
EC-INT-003 A contractor exists in Slack and Linear but not in the HRIS. Person from comms/work with no HRIS identity after backfill. Unmatched people are not managed until resolved (§3.7). Listed to owner as unresolved (PROPOSE_CONFIRM: add as contractor / ignore). None. Persona Alex (contractor); assert no manager thread opened before owner resolves.
EC-INT-004 Two HRIS records share one work email (rehire). Duplicate key on Person.identities.email. Anchor = record with employment_status=active; the other becomes Person.previous_employment. Never auto-merge on name. Merge proposal (PROPOSE_CONFIRM). Admin. Fixture with rehire; assert single active Person, history preserved.
EC-INT-005 A Linear project referenced by a Goal is deleted upstream. Daily reconcile finds the externalId missing. Tombstone, never delete; Goal edge flagged dangling. Chief Porpoise Officer proposes relink or retire Goal (PROPOSE_CONFIRM). Goal owner. FakeLinear.delete(project); assert Goal still resolves with dangling flag.
EC-INT-006 Three restaurant locations in two time zones; POS timestamps arrive in UTC, shifts in local. Location tz mismatch between POS and scheduling records. All atoms records normalized to location tz on ingestion; UTC stored alongside. Reject records with ambiguous tz to a quarantine queue (AUTONOMOUS). Ops if quarantine > 1% of records. chowder-house fixture with Airport location in a different tz; assert huddle times and labor math correct.
EC-INT-007 An issue moves from Team A to Team B in Linear. Same externalId, changed team. Identity is the externalId; Org attribution updates; stall clocks reset to the move time. Update attribution (AUTONOMOUS); no chase for 24h after a move. None. Move issue mid-week; assert no chase to the old assignee.
EC-INT-009 Admin grants the Slack app admin.* scopes by installing a wider manifest. Daily scope diff finds scopes not in manifest. R-SCOPE-1: pause until re-consent with the correct manifest. Connector → PAUSED (AUTONOMOUS); explain on the admin page. Tenant admin; Sea Suite security if repeated. FakeSlack.grantExtraScope("admin.users:write"); assert pause and audit event connector.scope_drift.
EC-INT-010 HRIS vendor adds a salary field to the employee payload. Unknown field in payload failing the allowlist. Connectors use field allowlists, not denylists; unknown fields are dropped and counted. Drop, emit connector.unknown_field metric (AUTONOMOUS). Security review if the unknown field name matches the sensitive lexicon. Fixture adds salary, dob; assert absent from evidence store and model context.
EC-INT-012 An SMS arrives from a number not linked to any Person. E.164 not in identities. Quarantine; no manager run; no reply beyond a fixed "this number isn't registered; contact " once per 24h. Quarantine (AUTONOMOUS). Manager of record if the number persists 3 days. FakeTwilio.inbound(unknown); assert zero model calls.
EC-INT-013 A human publishes shifts in 7shifts while the solver has a pending schedule proposal. Write conflict: expectedVersion mismatch on publish. Human publish supersedes pending solver proposal (isBenignSupersede). Withdraw proposal; re-solve against the published schedule; re-propose only deltas (PROPOSE_CONFIRM). Location manager of record if the human schedule violates labor rules (flagged, never auto-corrected). chowder-house W6; assert no double publish and a labor-rule flag if seeded.

9. Risk controls

RC-INT-1 — Over-scoped connector. Owner: Chief Security Officer. Metric: scope-drift events per month (target 0 unresolved > 24h). - Detect: daily scope diff (R-SCOPE-1); consent-screen scope list compared to manifest at install. - Contain: connector PAUSED; writes suspended; admin notified. - Correct: re-consent with correct manifest; if the manifest itself was wrong, hotfix connector version. - Learn: add the drifted scope to the never list with justification; add fixture to the scope-conformance test.

RC-INT-2 — Stale data drives an action. Owner: Chief Build Officer. Metric: actions executed with any receipt older than the freshness SLO (target 0 for chases; labeled-only for reports). - Detect: receipts carry maxSourceAge; rule stale_receipt fires when it exceeds the SLO for stale-sensitive actions. - Contain: downgrade or suspend per degradation profile; stall clocks pause. - Correct: reconcile on recovery before any run; drop expired queued messages. - Learn: every stale-driven action that reached a human becomes a scenario keyed to this control.

RC-INT-3 — Write-back clobbers a human edit. Owner: Chief Build Officer. Metric: conflict escalations per 1,000 writes; clobbers (target 0). - Detect: CAS on every write (expectedVersion); post-write read-back diff. - Contain: on conflict, abandon or escalate (§4.3); never retry blind more than once. - Correct: if a clobber is found by read-back, restore the human's version from the etag cache and notify the human with an apology receipt. - Learn: extend isBenignSupersede table or add a new conflict class; replay the recorded run in CI.

RC-INT-4 — Sensitive field leaks through a payload. Owner: Chief Security Officer. Metric: sensitive-lexicon hits in evidence store (target 0); unknown-field rate. - Detect: allowlists at ingestion; sensitive-lexicon scan on stored records nightly; connector.unknown_field metric. - Contain: purge the record from evidence store and any memory derived from it; block the field. - Correct: connector version bump with the field added to dropFields; retention job re-runs. - Learn: fixture added; lexicon updated; incident review (15-observability-and-runbooks.md).

10. Checklist for adding a new connector

A connector is installable by tenants only when every box is checked and the PR links the evidence.

  • [ ] Manifest: every scope tiered (core / feature:* / never) with a customer-readable justification and dataClassMax; reviewed by the Chief Security Officer domain owner. HUMAN_ONLY fields have no write scope at all.
  • [ ] Field allowlists (never denylists) per entity; dropFields covers compensation, government ids, DOB, home address, demographics, health/leave reasons, bank details, customer PII, card numbers. Free text is injection-scanned; low-signal free text (commit messages, item modifiers) is not ingested.
  • [ ] Mapping and authority rows added to §3 and §4.1, with personal vs sensitive-personal marked and a default write class per writable field.
  • [ ] Sync: backfill order and windows; incremental with overlap; webhook signature verification and dedupe; polling fallback; daily reconcile; delete → tombstone; idempotent upserts and writes tested with a redelivery fixture.
  • [ ] CAS writes with expectedVersion where the vendor supports it, read-back diff where it does not.
  • [ ] Rate limits, priorities, breaker; load-tested against a fake with the vendor's published limits.
  • [ ] Degradation profile and health sampling wired in; verified in the sim org by killing the fake mid-week.
  • [ ] Fake implementation with scripted-week hooks (14-evaluation-and-testing.md §1).
  • [ ] Identity resolution rules stated; no auto-merge on name.
  • [ ] "What my manager knows" strings rendered from the manifest and registered with the page (INV-READ-1).
  • [ ] Audit events for install, scope changes, state changes, every write; retention class per entity with deletion tested end to end.
  • [ ] Runbook signature (RB-01 variant) and edge cases with scenarios in 08-edge-case-catalog.md.
  • [ ] Vendor review: directory listing, 10DLC / marketplace requirements, terms permitting our use.
  • [ ] Shadow run: one internal tenant on SHADOW for 2 weeks with zero SEV2+ before general availability.

Open questions

  1. Meeting transcripts and recordings: is there a per-meeting consent design that satisfies "no covert surveillance", or do they stay permanently in never?
  2. Should the Atoms scheduling solver be SoR by default when Deputy/7shifts is connected? Current default: vendor is SoR, solver publishes through it.
  3. Accounting write-back is NEVER in v1. Does the Chief Fin Officer's Full Suite charter need bill-pay scheduling, and at what class?
  4. MCP-first: which v1 vendors have MCP servers mature enough that the connector becomes a thin manifest over MCP, and does that change the sandbox model?