Data model and API
Purpose¶
This is the reference the other docs import from: TypeScript-style definitions for every core type, the event catalog (names, payloads, producers, consumers), the REST and GraphQL endpoints for the IC surface, the Owner Console and admin, the webhook contracts for inbound integrations and outbound customer subscriptions, and the error codes. Where a type is specified in depth elsewhere (Org Model nodes in Org Model Spec, run internals in Harness and Orchestration), the definition here is the canonical wire shape and the other doc is the semantics. When they disagree, this doc is wrong and should be fixed to match.
Scope / Non-scope¶
In scope
- Conventions: identifiers, time, versioning, pagination, authentication, tenancy, idempotency.
- Core types: tenancy and identity, Org Model, runs and events, decisions and receipts, memory, outreach and threads, escalations and handoffs, graduation, integrations, content, audit.
- Event catalog with payloads, producers, consumers.
- Endpoints:
/v1/me(IC surface),/v1/owner(Owner Console),/v1/admin,/v1/graphql. - Inbound webhook contracts per provider family; outbound webhook contract.
- Error codes with HTTP mapping and retry guidance.
Non-scope
- Field-level connector mappings → Integrations Spec.
- Policy rule AST → Policy Engine.
- Auth threat model, key management → Security, Privacy, Compliance.
- UI behavior of the console and app.
1. Conventions¶
| Topic | Rule |
|---|---|
| Identifiers | Prefixed ULIDs: tnt_ prs_ rol_ org_ gol_ prj_ cad_ pol_ dec_ edg_ chg_ snp_ run_ evt_ rcp_ mem_ esc_ hnd_ thr_ msg_ out_ pkt_ whk_ int_ cnt_ aud_. Prefix mismatches are E_VALIDATION. |
| Time | ISO 8601 UTC with milliseconds on the wire (2026-09-03T14:05:00.000Z); local times only inside briefWindow/quietHours as HH:mm with an IANA timezone. |
| Versioning | Path-versioned: /v1. Additive changes do not bump; removals and semantic changes do. Event and webhook payloads carry schemaVersion. |
| Pagination | Cursor-based: ?limit=50&cursor=…; responses carry nextCursor (opaque, expires in 24 h). Max limit 200. |
| Authentication | Humans: OIDC session (console, app, admin) → bearer JWT with sub=prs_…, tenant=tnt_…, roles. Surfaces: adapter-authenticated (Slack user id → Person via identity), never user tokens. Automation: tenant-scoped API keys with named scopes (org:read, org:write, runs:read, audit:export, webhooks:manage); API keys can never decide HUMAN_ONLY packets or approve PROPOSE_CONFIRM decisions. Internal services: mTLS. |
| Tenancy | Every request resolves exactly one tenant from the token; X-Tenant-Id is optional and must match (E_TENANT_MISMATCH otherwise). Portfolio grants switch tenant per request via X-Tenant-Id. |
| Idempotency | All POSTs accept Idempotency-Key (UUID). The same key with the same body returns the original response with Idempotent-Replayed: true; a different body is E_IDEMPOTENCY_CONFLICT. Keys are retained 7 days. |
| Concurrency | Writes to Org Model nodes accept If-Match: <version>; mismatch is E_ORG_VERSION_CONFLICT. |
| Reasons | Every mutating request that changes the Org Model, a policy, a graduation state, or memory requires reason (non-empty string). |
| Errors | { "error": { "code": string, "message": string, "details"?: object, "requestId": string, "docs": string } }. |
| Rate limits | Per tenant per key: 600 req/min read, 120 req/min write; Retry-After on 429. |
2. Core types¶
2.1 Scalars and identity¶
type TenantId = `tnt_${string}`; type PersonId = `prs_${string}`; type RoleId = `rol_${string}`;
type OrgId = `org_${string}`; type GoalId = `gol_${string}`; type ProjectId = `prj_${string}`;
type CadenceId = `cad_${string}`; type PolicyId = `pol_${string}`; type DecisionId = `dec_${string}`;
type EdgeId = `edg_${string}`; type ChangeId = `chg_${string}`; type SnapshotId = `snp_${string}`;
type RunId = `run_${string}`; type EventId = `evt_${string}`; type ReceiptId = `rcp_${string}`;
type MemoryFactId = `mem_${string}`; type EscalationId = `esc_${string}`; type HandoffId = `hnd_${string}`;
type ThreadId = `thr_${string}`; type MessageId = `msg_${string}`; type OutreachId = `out_${string}`;
type PacketId = `pkt_${string}`; type WebhookId = `whk_${string}`; type IntegrationId = `int_${string}`;
type ContentId = `cnt_${string}`; type AuditId = `aud_${string}`;
type ISODateTime = string; type ISODate = string;
type ChonkId = 'chonksworth' | 'fin' | 'ops' | 'human' | 'porpoise' | 'build' | 'marketing' | 'revenue' | 'security';
type AutonomyClass = 'AUTONOMOUS' | 'PROPOSE_CONFIRM' | 'HUMAN_ONLY';
type ExecutionMode = 'SHADOW' | 'PROPOSE_CONFIRM' | 'AUTONOMOUS' | 'HUMAN_ONLY' | 'BLOCKED';
type GraduationState = 'SHADOW' | 'PROPOSE' | 'AUTONOMOUS';
type Surface = 'slack' | 'teams' | 'sms' | 'voice' | 'app' | 'email' | 'console';
type ManagementFunction =
| 'goal_setting' | 'planning' | 'checkins_unblocking' | 'feedback_growth' | 'staffing_scheduling'
| 'hiring_onboarding' | 'budget_approvals' | 'reporting_escalation' | 'rituals_comms' | 'quiet_hr' | 'security_compliance';
type ConnectorProvider =
| 'linear' | 'jira' | 'github' | 'asana' | 'notion' | 'gdrive'
| 'slack' | 'msteams' | 'gmail' | 'gcal' | 'outlook' | 'twilio'
| 'gusto' | 'rippling' | 'deel' | 'bamboohr' | 'workday'
| 'quickbooks' | 'xero' | 'ramp' | 'brex' | 'stripe' | 'hubspot' | 'intercom'
| 'square' | 'toast' | 'shopify_pos' | 'deputy' | 'wheniwork' | 'sevenshifts' | 'homebase' | 'timeclock' | 'iot'
| 'mcp';
interface Actor { kind: 'human' | 'run' | 'system' | 'api_key'; id: string; onBehalfOf?: PersonId }
interface NodeRef { kind: NodeKind; id: string }
interface ExternalRef { provider: ConnectorProvider; externalId: string; url?: string; lastSyncedAt: ISODateTime }
2.2 Tenant¶
interface Tenant {
id: TenantId;
name: string;
mode: 'FULL_SUITE' | 'ORG' | 'FUNCTION';
atoms: boolean;
phase: 'ONBOARDING' | 'LISTENING' | 'ACTIVE' | 'PAUSED' | 'OFFBOARDING';
listening?: { startedAt: ISODateTime; endsAt: ISODateTime };
scope: { orgIds?: OrgId[]; functions?: ManagementFunction[] }; // ORG / FUNCTION modes
cellId: string;
schemaVersion: string;
plan: { perPersonDailyModelUsd: number; managedPeopleCap?: number };
retention: Record<'work_signal' | 'feedback' | 'preference' | 'consideration' | 'decision_log' | 'recognition' | 'audit', string>; // ISO 8601 durations or 'until_offboarded+P30D'
modelProvider: { tiers: Record<'fast' | 'standard' | 'reasoning', string[]>; zeroRetentionOnly: true };
createdAt: ISODateTime;
}
interface PortfolioGrant { granteeId: PersonId; tenantId: TenantId; scope: 'health' | 'approvals' | 'full'; grantedBy: PersonId; grantedAt: ISODateTime }
2.3 Org Model (wire shapes; semantics in doc 02)¶
type NodeKind = 'Person' | 'Role' | 'Org' | 'Goal' | 'Project' | 'Cadence' | 'Policy' | 'Decision';
interface NodeBase { id: string; tenantId: TenantId; kind: NodeKind; version: number; validFrom: ISODateTime; validTo?: ISODateTime; createdAt: ISODateTime; updatedAt: ISODateTime; labels?: string[]; externalRefs?: ExternalRef[] }
interface Identity { provider: ConnectorProvider; externalId: string; email?: string; handle?: string; displayName?: string; verified: boolean; linkedAt: ISODateTime; linkedBy: Actor; confidence: number }
interface Person extends NodeBase {
kind: 'Person'; personKind: 'human' | 'agent';
status: 'invited' | 'active' | 'on_leave' | 'offboarding' | 'offboarded' | 'merged';
displayName: string; givenName?: string; familyName?: string; pronouns?: string; timezone: string; locale: string;
identities: Identity[];
contact: { preferredSurface: Surface; surfaceOrder: Surface[]; workEmail?: string; phoneE164?: string };
briefWindow?: { start: string; end: string }; quietHours?: { start: string; end: string };
employment?: { type: 'employee' | 'contractor' | 'intern' | 'agent_worker'; startDate: ISODate; endDate?: ISODate; laborClass?: 'exempt' | 'non_exempt' | 'hourly'; hrisRef?: ExternalRef; isMinor?: boolean };
agent?: { agentKind: 'chonk' | 'worker'; chonkId?: ChonkId; endpoint?: string; charterRef?: string };
isOwner: boolean; mergedInto?: PersonId;
}
interface Role extends NodeBase { kind: 'Role'; title: string; family: string; band?: string; responsibilities: string[]; skills?: string[]; certifications?: string[]; shiftEligible?: boolean; headcountPlan?: { period: string; planned: number } }
interface Org extends NodeBase { kind: 'Org'; name: string; orgKind: 'company' | 'department' | 'team' | 'location' | 'shift_group' | 'portfolio_slot'; timezone: string; managedFunctions?: ManagementFunction[]; location?: { address: string; operatingHours: Array<{ dow: number; open: string; close: string }>; laborJurisdiction: string; posRef?: ExternalRef; schedulingRef?: ExternalRef }; parentId?: OrgId; managerChonkId?: ChonkId; managerOfRecordId?: PersonId }
interface Goal extends NodeBase { kind: 'Goal'; title: string; description?: string; level: 'company' | 'org' | 'person'; period: { start: ISODate; end: ISODate }; metric?: { name: string; unit: string; target: number; baseline?: number; current?: number; direction: 'up' | 'down'; sourceRef?: ExternalRef }; status: 'draft' | 'proposed' | 'active' | 'achieved' | 'missed' | 'cancelled'; health: 'green' | 'yellow' | 'red' | 'unknown'; healthReason?: string; healthAsOf?: ISODateTime; sandbagFlag?: { flaggedAt: ISODateTime; reason: string; receiptId: ReceiptId }; ownerId?: PersonId; reviewCadenceId?: CadenceId }
interface Project extends NodeBase { kind: 'Project'; title: string; status: 'proposed' | 'active' | 'blocked' | 'paused' | 'done' | 'cancelled'; health: 'green' | 'yellow' | 'red' | 'unknown'; healthReason?: string; dates: { start?: ISODate; target?: ISODate; actualEnd?: ISODate }; estimate?: { unit: 'person_weeks' | 'points' | 'shifts'; value: number; confidence: number }; scope?: string; orgId?: OrgId; ownerId?: PersonId }
type CadenceKind = 'morning_brief' | 'async_checkin' | 'one_on_one' | 'daily_standup' | 'weekly_review' | 'monthly_retro' | 'quarterly_planning' | 'all_hands' | 'owner_update' | 'goal_review' | 'pipeline_review' | 'shift_huddle' | 'schedule_publish' | 'checklist' | 'custom';
interface Cadence extends NodeBase { kind: 'Cadence'; name: string; cadenceKind: CadenceKind; ownerChonkId: ChonkId; format: 'written' | 'video' | 'live' | 'sms' | 'voice' | 'comic'; schedule: { rrule: string; timezone: string; window?: { start: string; end: string }; graceMinutes: number }; scope: { orgId?: OrgId; personId?: PersonId }; inputs: Array<{ kind: 'org_model' | 'mirror' | 'memory' | 'reply'; spec: string }>; outputs: Array<{ kind: 'message' | 'document' | 'video' | 'decision' | 'org_change'; spec: string }>; status: 'active' | 'paused' | 'retired'; effectiveness?: { replyRate30d?: number; openRate30d?: number; lastTunedAt?: ISODateTime; tuneReason?: string }; priority: 'P0' | 'P1' | 'P2' | 'P3' | 'P4' | 'P5' }
type PolicyKind = 'hard_line' | 'threshold' | 'autonomy_override' | 'outreach_cap' | 'labor_rule' | 'approval_route' | 'retention' | 'tone';
interface Policy extends NodeBase { kind: 'Policy'; name: string; policyKind: PolicyKind; rule: PolicyRule; autonomyClass?: AutonomyClass; precedence: number; setBy: PersonId; effective: { from: ISODateTime; to?: ISODateTime }; appliesTo?: Array<{ kind: 'Org' | 'Role' | 'Person' | 'Function'; id: string }> }
type PolicyRule = Record<string, unknown>; // AST defined in doc 05
type DecisionKind = 'spend_approval' | 'plan_change' | 'goal_set' | 'req_open' | 'cadence_change' | 'escalation_to_owner' | 'schedule_exception' | 'org_change' | 'identity_merge' | 'human_only_packet' | 'graduation_change' | 'custom';
interface Decision extends NodeBase { kind: 'Decision'; decisionKind: DecisionKind; autonomyClass: AutonomyClass; effectiveMode: ExecutionMode; proposedByChonkId: ChonkId; runId: RunId; summary: string; recommendation?: string; receiptId: ReceiptId; status: 'proposed' | 'confirmed' | 'rejected' | 'executed' | 'expired' | 'recorded' | 'stale' | 'shadow'; routedTo?: PersonId[]; decidedBy?: PersonId; decidedAt?: ISODateTime; humanNote?: string; expiresAt?: ISODateTime; outcome?: { observedAt: ISODateTime; summary: string; receiptId?: ReceiptId } }
type EdgeType = 'member_of' | 'holds_role' | 'child_of' | 'managed_by' | 'manager_of_record' | 'reports_to' | 'escalates_to' | 'owns' | 'contributes_to' | 'depends_on' | 'reviewed_by' | 'participates_in' | 'applies_to' | 'about' | 'backs_up' | 'scheduled_at' | 'assigned_to';
interface Edge { id: EdgeId; tenantId: TenantId; type: EdgeType; from: NodeRef; to: NodeRef; attrs: Record<string, unknown>; version: number; validFrom: ISODateTime; validTo?: ISODateTime; reason: string; actor: Actor }
type ChangeOp =
| { op: 'create_node'; node: NodeBase } | { op: 'update_node'; id: string; patch: JsonPatch; before?: object }
| { op: 'end_node'; id: string; validTo: ISODateTime; newStatus?: string }
| { op: 'create_edge'; edge: Omit<Edge, 'id' | 'version' | 'validFrom' | 'actor' | 'reason'> }
| { op: 'end_edge'; id: EdgeId; validTo: ISODateTime }
| { op: 'merge_person'; from: PersonId; into: PersonId; edgeMap?: Array<{ edgeId: EdgeId; originalFrom: PersonId }> };
interface Change { id: ChangeId; tenantId: TenantId; version: number; at: ISODateTime; actor: Actor; reason: string; reasonCode?: string; runId?: RunId; decisionId?: DecisionId; ops: ChangeOp[]; invariantsChecked: string[]; schemaVersion: string }
interface MergeProposal { id: string; tenantId: TenantId; a: PersonId; b: PersonId; rule: 'M3' | 'M4' | 'M5'; conflict: boolean; evidence: Array<{ provider: ConnectorProvider; detail: string; score: number }>; status: 'open' | 'accepted' | 'rejected'; decidedBy?: PersonId; decidedAt?: ISODateTime }
2.4 Runs and events¶
type TriggerKind = 'CADENCE_TICK' | 'INTEGRATION_EVENT' | 'IC_MESSAGE' | 'ESCALATION' | 'HANDOFF' | 'HUMAN_DECISION' | 'FOLLOWUP' | 'REPLAY' | 'MANUAL';
type Trigger =
| { kind: 'CADENCE_TICK'; cadenceId: CadenceId; fireAt: ISODateTime; participantId?: PersonId }
| { kind: 'INTEGRATION_EVENT'; provider: ConnectorProvider; deliveryId: string; eventType: string; subjectRefs: NodeRef[]; signal?: Signal }
| { kind: 'IC_MESSAGE'; surface: Surface; threadId: ThreadId; messageId: MessageId; personId: PersonId }
| { kind: 'ESCALATION'; escalationId: EscalationId; stage: number }
| { kind: 'HANDOFF'; handoffId: HandoffId; fromChonk: ChonkId; toChonk: ChonkId }
| { kind: 'HUMAN_DECISION'; decisionId: DecisionId; status: 'confirmed' | 'rejected' | 'expired'; actorId?: PersonId; resumeRunId?: RunId }
| { kind: 'FOLLOWUP'; parentRunId: RunId; stage: number; dueAt: ISODateTime }
| { kind: 'REPLAY'; sourceRunId: RunId; mode: 'VERIFY' | 'SHADOW_REEXEC' | 'LIVE_REEXEC'; nonce: string }
| { kind: 'MANUAL'; actorId: PersonId; objective: string; clientNonce: string };
interface Signal { id: string; kind: 'stall' | 'variance' | 'uncovered_shift' | 'callout' | 'goal_drift' | 'capacity' | 'anomaly' | 'compliance_due'; ruleId: string; subjectRefs: NodeRef[]; detectedAt: ISODateTime; evidence: SourceRef[]; severity: 'info' | 'watch' | 'risk' | 'critical' }
type RunKind = 'morning_brief' | 'ic_reply' | 'checkin' | 'one_on_one' | 'chase' | 'followup' | 'weekly_review' | 'owner_update' | 'quarterly_planning' | 'replan' | 'approval' | 'schedule_publish' | 'shift_swap' | 'checklist' | 'escalation' | 'handoff' | 'onboarding_draft' | 'listening_intro' | 'content_render' | 'integrity_check' | 'custom';
type RunState = 'REQUESTED' | 'DEDUPLICATED' | 'COALESCED' | 'QUEUED' | 'DEFERRED' | 'EXPIRED' | 'ADMITTED' | 'RUNNING' | 'WAITING_HUMAN' | 'WAITING_EXTERNAL' | 'SUCCEEDED' | 'BUDGET_EXCEEDED' | 'FAILED_RETRYABLE' | 'FAILED' | 'DEAD_LETTER' | 'CANCELLED';
type FailureClass = 'MODEL_TRANSIENT' | 'MODEL_OUTPUT_INVALID' | 'TOOL_TRANSIENT' | 'CONNECTOR_RATE_LIMITED' | 'CONNECTOR_UNAVAILABLE' | 'CONNECTOR_AUTH' | 'POLICY_BLOCKED' | 'BUDGET_EXCEEDED' | 'ORG_INVARIANT' | 'LEASE_LOST' | 'SURFACE_DELIVERY' | 'INTERNAL';
interface RunBudget { maxModelCalls: number; maxInputTokens: number; maxOutputTokens: number; maxWallMs: number; maxToolCalls: number; maxOutreach: number; maxCostUsd: number }
interface Usage { modelCalls: number; inputTokens: number; outputTokens: number; cachedTokens: number; wallMs: number; toolCalls: number; outreach: number; costUsd: number }
interface Run {
id: RunId; tenantId: TenantId; chonkId: ChonkId; runKind: RunKind; trigger: Trigger;
idempotencyKey: string; subjectKey: string; priority: 'P0' | 'P1' | 'P2' | 'P3' | 'P4' | 'P5';
deadline?: ISODateTime; expirable: boolean; coalescable: boolean; budget: RunBudget;
state: RunState; attempt: number; orgModelVersion: number; functionModes: Partial<Record<ManagementFunction, ExecutionMode>>;
requestedAt: ISODateTime; startedAt?: ISODateTime; endedAt?: ISODateTime; usage: Usage;
pendingDecisions: DecisionId[]; failure?: { class: FailureClass; message: string; retryable: boolean };
parentRunId?: RunId; correlationId: string; replay?: { sourceRunId: RunId; mode: string }; degraded?: boolean;
}
interface Event<T extends string = string, P = unknown> { id: EventId; tenantId: TenantId; seq: number; ts: ISODateTime; type: T; payload: P; runId?: RunId; stepId?: string; correlationId: string; causationId?: EventId; actor: Actor; schemaVersion: string; hash: string }
type ActionKind = 'SEND_MESSAGE' | 'ORG_CHANGE' | 'CONNECTOR_WRITE' | 'SCHEDULE_FOLLOWUP' | 'HANDOFF' | 'ESCALATE' | 'RENDER_CONTENT' | 'OPEN_REQ' | 'PACKET' | 'APPROVE_SPEND' | 'SCHEDULE_SHIFT' | 'SWAP_SHIFT' | 'OFFER_DISCOUNT' | 'BOOK_MEETING';
interface ProposedAction { id: string; kind: ActionKind; subKind?: string; args: Record<string, unknown>; subjectRefs: NodeRef[]; receiptId: ReceiptId; declaredClass: AutonomyClass; fn: ManagementFunction; reason: string }
interface Verdict { mode: ExecutionMode; reasons: RuleEvaluation[]; gates: string[]; policyIds: PolicyId[]; narrowedBy?: PolicyId; decidedBy?: PersonId }
2.5 Receipts, packets, escalations, handoffs¶
interface SourceRef { n: number; kind: 'org' | 'mirror' | 'memory' | 'service' | 'policy' | 'message' | 'decision'; ref: string; label: string; asOf: ISODateTime; url?: string }
interface RuleEvaluation { ruleId: string; input: Record<string, unknown>; result: 'pass' | 'fail' | 'threshold_exceeded' | 'n/a'; detail: string }
interface Receipt { id: ReceiptId; runId: RunId; tenantId: TenantId; createdAt: ISODateTime; sources: SourceRef[]; snapshot: Array<{ source: ConnectorProvider | 'org_model' | 'memory'; asOf: ISODateTime; staleSince?: ISODateTime; version?: number }>; ruleEvaluations: RuleEvaluation[]; rationale: string; confidence: number; gaps: string[]; modelCalls: Array<{ purpose: string; tier: string; requestHash: string }>; visibility: 'self' | 'self_mor' | 'self_owner' | 'owner' }
interface Packet { id: PacketId; tenantId: TenantId; decisionId: DecisionId; kind: 'hiring' | 'termination' | 'compensation' | 'promotion' | 'discipline' | 'legal' | 'safety' | 'protected_characteristic' | 'other'; subjectRefs: NodeRef[]; preparedByChonkId: ChonkId; routedTo: PersonId[]; contents: { summary: string; facts: SourceRef[]; options?: string[]; noRecommendation: true }; accessLog: Array<{ personId: PersonId; at: ISODateTime }>; humanDecision?: { by: PersonId; at: ISODateTime; text: string }; status: 'sent' | 'viewed' | 'recorded' }
interface Escalation { id: EscalationId; tenantId: TenantId; kind: 'IC_TO_HUMAN' | 'CADENCE_MISSED' | 'DEPENDENCY_UNRESPONSIVE' | 'POLICY_VIOLATION' | 'SAFETY' | 'INTEGRITY' | 'CONNECTOR' | 'BUDGET' | 'OWNER_DECISION'; severity: 'low' | 'medium' | 'high' | 'critical'; openedBy: Actor; openedAt: ISODateTime; subjectRefs: NodeRef[]; scopeOrgId?: OrgId; route: Array<{ stage: number; to: PersonId; via: Surface[]; dueAt: ISODateTime; deliveredAt?: ISODateTime; acknowledgedAt?: ISODateTime }>; receiptId?: ReceiptId; message?: string; suppressible: false; status: 'open' | 'acknowledged' | 'resolved'; resolvedBy?: PersonId; resolvedAt?: ISODateTime; resolutionNote?: string }
type HandoffReason = 'people_problem' | 'budget_impact' | 'scope_change' | 'security_or_safety' | 'owner_escalation' | 'cross_org_dependency' | 'recognition' | 'quiet_hr' | 'deal_desk' | 'other';
interface Handoff { id: HandoffId; tenantId: TenantId; fromChonk: ChonkId; toChonk: ChonkId; reason: HandoffReason; subjectRefs: NodeRef[]; requestedAction: string; urgency: 'now' | 'today' | 'this_week'; contextBundleRef: string; receiptId: ReceiptId; correlationId: string; createdByRunId: RunId; status: 'requested' | 'accepted' | 'declined' | 'completed'; declineReason?: string }
2.6 Memory¶
type MemoryFactKind = 'preference' | 'working_agreement' | 'commitment' | 'blocker_history' | 'signal' | 'feedback_given' | 'feedback_received' | 'growth_plan' | 'recognition' | 'consideration' | 'decision_ref' | 'owner_report';
interface MemoryFact { id: MemoryFactId; tenantId: TenantId; subjectId: PersonId; kind: MemoryFactKind; content: string; source: SourceRef; sharedBy: 'self' | 'work_system' | 'chonk' | PersonId; visibility: 'self' | 'self_mor' | 'self_owner' | 'org_public'; retentionClass: 'work_signal' | 'feedback' | 'preference' | 'consideration' | 'decision_log' | 'recognition'; expiresAt: ISODateTime; writtenByRunId: RunId; writtenByChonkId: ChonkId; usedFor: string[]; subjectResponse?: { at: ISODateTime; text: string }; redactedAt?: ISODateTime }
interface KnowsPage { personId: PersonId; generatedAt: ISODateTime; facts: MemoryFact[]; edges: Edge[]; decisions: Decision[]; sources: Array<{ provider: ConnectorProvider; whatIsRead: string[]; whatIsNotRead: string[] }>; accessLog: Array<{ at: ISODateTime; chonkId: ChonkId; runKind: RunKind; factIds: MemoryFactId[] }>; escalationChain: Array<{ personId: PersonId; displayName: string; orgId: OrgId }> }
2.7 Outreach and threads¶
interface Thread { id: ThreadId; tenantId: TenantId; personId: PersonId; surface: Surface; providerRef: { provider: ConnectorProvider; channelId: string; threadTs?: string }; status: 'active' | 'read_only' | 'closed'; createdAt: ISODateTime }
interface Message { id: MessageId; tenantId: TenantId; threadId: ThreadId; direction: 'inbound' | 'outbound'; authorId: PersonId; body: string; attachments?: Array<{ kind: 'receipt' | 'video' | 'image' | 'document'; ref: string }>; receiptId?: ReceiptId; providerMessageId?: string; sentAt: ISODateTime; runId?: RunId }
type OutreachKind = 'brief' | 'checkin' | 'reply' | 'chase' | 'status' | 'feedback' | 'approval_request' | 'decision_notice' | 'escalation' | 'packet_notice' | 'schedule' | 'huddle' | 'checklist' | 'celebration' | 'intro' | 'owner_update' | 'fallback' | 'system';
interface OutreachRequest { id: OutreachId; tenantId: TenantId; runId: RunId; actionId: string; recipientId: PersonId; kind: OutreachKind; context: 'safety' | 'human_only_packet' | 'wellbeing' | 'feedback_concern' | 'rejection_notice' | 'escalation' | 'incident' | 'approval_request' | 'plan_change' | 'brief' | 'checkin' | 'reply' | 'review' | 'owner_update' | 'celebration' | 'all_hands' | 'launch'; body: string; receiptId: ReceiptId; toneUsed: number; surfaceHint?: Surface; aboutPersonIds?: PersonId[]; exempt?: 'escalation' | 'safety' | 'callout' | 'cadence_missed'; coalesceKey?: string }
interface OutreachResult { id: OutreachId; status: 'delivered' | 'queued' | 'suppressed' | 'failed' | 'coalesced'; surface?: Surface; messageId?: MessageId; reason?: 'cap_messages' | 'cap_chases' | 'quiet_hours' | 'tone_ceiling' | 'thread_read_only' | 'surface_unavailable'; retryAt?: ISODateTime }
2.8 Graduation, integrations, content, audit¶
interface Graduation { tenantId: TenantId; fn: ManagementFunction; state: GraduationState; since: ISODateTime; ownerCap?: GraduationState; stats: { runs: number; proposals: number; confirmedUnedited: number; edited: number; rejected: number; overrides: number; incidents: number; windowStart: ISODateTime }; history: Array<{ at: ISODateTime; from: GraduationState; to: GraduationState; reason: string; actor: Actor }> }
interface ShadowLogEntry { id: string; tenantId: TenantId; runId: RunId; chonkId: ChonkId; fn: ManagementFunction; action: ProposedAction; verdict: Verdict; renderedBody?: string; review?: { by: PersonId; at: ISODateTime; verdict: 'would_accept' | 'would_edit' | 'would_reject'; note?: string } }
interface Integration { id: IntegrationId; tenantId: TenantId; provider: ConnectorProvider; kind: 'mirror' | 'actuator' | 'both'; scopes: string[]; status: 'connected' | 'degraded' | 'needs_reauth' | 'disconnected'; health: { lastEventAt?: ISODateTime; lastSyncAt?: ISODateTime; staleSince?: ISODateTime; errorRate1h: number }; feeds: ManagementFunction[]; connectedBy: PersonId; connectedAt: ISODateTime }
interface IntegrationEvent { id: string; tenantId: TenantId; provider: ConnectorProvider; deliveryId: string; eventType: string; receivedAt: ISODateTime; occurredAt?: ISODateTime; subjectRefs: NodeRef[]; externalRefs: ExternalRef[]; payloadHash: string; normalized: Record<string, unknown> }
interface ContentJob { id: ContentId; tenantId: TenantId; runId: RunId; format: 'text' | 'video' | 'comic' | 'card' | 'audio'; chonkId: ChonkId; script: string; citations: SourceRef[]; voiceProfileRef: string; toneUsed: number; template: string; status: 'queued' | 'rendering' | 'ready' | 'failed'; assetUrl?: string; durationSec?: number; receiptId: ReceiptId }
interface AuditEntry { id: AuditId; tenantId: TenantId; seq: number; ts: ISODateTime; actor: Actor; action: 'policy.verdict' | 'outreach.sent' | 'data.access' | 'org.change' | 'memory.write' | 'memory.redact' | 'decision.human' | 'connector.write' | 'export' | 'purge' | 'graduation.change' | 'escalation.open' | 'system.degraded'; subject: NodeRef[]; why: string; receiptId?: ReceiptId; runId?: RunId; prevHash: string; hash: string }
interface AuditExport { id: string; tenantId: TenantId; requestedBy: PersonId; range: { from: ISODateTime; to: ISODateTime }; format: 'jsonl'; status: 'queued' | 'ready' | 'expired'; url?: string; chainHead: string; anchoredAt?: ISODateTime }
3. Event catalog¶
All events use the Event envelope. Producers write; consumers are idempotent on (tenantId, seq). "RM" = read models; "AUD" = audit-service; "SCH" = scheduler; "EXE" = executor; "OUT" = outreach-service; "ESC" = escalation-router; "MET" = metrics; "WHK" = outbound webhooks (only events marked ext).
| Event type | Payload (key fields) | Producer | Consumers |
|---|---|---|---|
org.node.created / org.node.updated / org.node.ended |
{ changeId, node: NodeBase, patch?, reason, actor } |
orgmodel-api | RM, AUD, SCH (cadence index), MET |
org.edge.created / org.edge.ended |
{ changeId, edge: Edge } |
orgmodel-api | RM, AUD, SCH |
org.change.applied |
{ change: Change } |
orgmodel-api | AUD, RM |
org.person.merged / org.person.unmerged |
{ from, into, edgeMap, reason } |
orgmodel-api | memory-service (rekey), surface adapters, AUD, WHK ext |
org.person.offboarding_started / org.person.offboarded |
{ personId, endDate, reason } |
orgmodel-api | memory-service (retention), OUT (thread read-only), SCH, AUD, WHK ext |
org.merge_proposal.opened / .decided |
{ proposal: MergeProposal } |
matcher | admin UI, AUD |
org.import.rejected |
{ provider, externalRef, invariant, detail } |
orgmodel-api | admin UI, Chief Build Officer weekly (via RM) |
org.snapshot.created / org.migration.applied |
{ snapshotId | migrationId, version } |
orgmodel-api | AUD, ops |
run.requested / run.queued / run.deduplicated / run.coalesced / run.deferred / run.expired |
{ run: Run } / { runId, original } / { runId, into } / { runId, resumeAt } |
SCH | MET, admin UI |
run.admitted / run.started / run.step.started / run.step.completed |
{ run } / { runId, attempt, resume } / { stepId, kind } / { stepId, outputHash, usage, modelCalls } |
SCH / EXE | MET, replay index |
run.waiting / run.resumed |
{ runId, reason: 'human' | 'external', pending } |
EXE | SCH (lease release), admin UI |
run.succeeded / run.failed / run.budget_exceeded / run.cancelled / run.dead_lettered |
{ runId, usage, degraded? } / { runId, class, message } / { runId, dimension, usage } |
EXE / SCH | MET, AUD, ESC (dead-letter, windowed failures), WHK ext (run.dead_lettered) |
tool.called |
{ runId, stepId, toolId, argsHash, resultHash, sources: SourceRef[] } |
tool gateway | AUD (data.access), replay index |
cadence.due / cadence.executed / cadence.skipped / cadence.missed / cadence.degraded / cadence.tuned |
{ cadenceId, participantId?, fireAt, runId?, reason? } |
SCH / EXE | ESC (missed), MET, Owner Console health, WHK ext (missed) |
integration.event.received |
{ event: IntegrationEvent } |
ingress | RM, detectors, AUD |
integration.sync.completed / .connector.degraded / .connector.restored / .connector.needs_reauth |
{ provider, stats | staleSince | error } |
connectors | admin UI, ESC (degraded > 6 h), Owner Console header, WHK ext |
signal.detected / signal.cleared |
{ signal: Signal } / { signalId, reason } |
detectors | SCH (run request), RM, MET |
policy.evaluated |
{ runId, action: ProposedAction, verdict: Verdict } |
EXE / OUT | AUD, MET, graduation stats |
policy.violation_blocked |
{ runId, action, verdict } |
EXE | AUD, graduation (regress), ESC, WHK ext |
policy.changed |
{ policyId, before, after, reason, actor } |
orgmodel-api | policy cache, AUD, WHK ext |
action.executed / action.shadowed |
{ runId, action, verdict, receiptId, result? } |
EXE | AUD, shadow log, MET |
decision.proposed / .confirmed / .rejected / .executed / .expired / .stale / .recorded / .overridden |
{ decision: Decision, actor? } |
EXE / console-api | OUT (notices), SCH (timers), AUD, graduation stats, WHK ext (all) |
decision.outcome_observed |
{ decisionId, outcome } |
analyst runs | AUD, Owner Console |
outreach.requested / .sent / .suppressed / .failed / .coalesced / .replied |
{ request: OutreachRequest, result: OutreachResult } / { messageId, personId } |
OUT / surface adapters | AUD (sent), MET, RM (thread), memory (owner_report on symmetry) |
escalation.opened / .routed / .delivered / .acknowledged / .resolved / .unacknowledged |
{ escalation: Escalation, stage? } |
ESC | OUT, Owner Console, AUD, WHK ext (opened, resolved, unacknowledged) |
handoff.requested / .accepted / .declined / .completed |
{ handoff: Handoff } |
EXE | SCH (run request), AUD, Chonksworth triage (declined ×2) |
memory.fact.written / .redacted / .expired / .rekeyed |
{ fact: MemoryFact } / { factId, by, reason } |
memory-service | AUD, knows page RM |
memory.read |
{ subjectId, chonkId, runKind, factIds } |
memory-service | AUD (data.access), knows page access log |
graduation.advanced / .regressed / .capped |
{ fn, from, to, reason, actor, receiptId? } |
harness / console-api | policy cache, OUT (owner + MoR notice), AUD, WHK ext |
listening.started / listening.ended |
{ tenantId, startedAt, endsAt | endedAt, reason? } |
console-api / SCH | policy cache, OUT (intro runs), AUD, WHK ext |
tenant.paused / tenant.resumed / tenant.backpressure |
{ reason } / { level } |
admin-api / SCH | SCH, OUT, admin UI |
content.rendered / content.failed |
{ job: ContentJob } |
content-engine | EXE (resume WAITING_EXTERNAL), OUT |
audit.appended / audit.anchor.written / audit.chain.broken / audit.export.ready |
{ entryId, seq } / { head, anchoredAt } / { atSeq, expected, found } / { export: AuditExport } |
AUD | ESC (chain.broken → critical), admin UI, WHK ext (export.ready) |
system.degraded / system.restored |
{ domain: 'model' | 'connector' | 'surface' | 'db' | 'scheduler' | 'executor' | 'event_log' | 'audit_anchor', detail } |
health probes | SCH (backpressure), Owner Console header, admin, WHK ext |
4. REST endpoints¶
Notation: AUTH = required principal; → = response type. All list endpoints paginate. All POST/PUT/PATCH accept Idempotency-Key.
4.1 IC surface — /v1/me¶
AUTH: the person (session or surface-authenticated).
| Method | Path | Body | → | Notes |
|---|---|---|---|---|
| GET | /v1/me |
— | { person: Person, manager: { chonkId, displayName }, managerOfRecord: Person, escalationChain: Array<{personId, displayName, orgId}>, tenantPhase } |
The thread header data |
| GET | /v1/me/brief?date= |
— | { brief: Message, receipt: Receipt, degraded: boolean } |
Today's by default |
| GET | /v1/me/thread/messages?cursor= |
— | { items: Message[], nextCursor } |
App surface only; Slack/Teams/SMS use their own history |
| POST | /v1/me/thread/messages |
{ body: string, clientNonce: string } |
{ message: Message, runId: RunId } |
Creates an IC_MESSAGE run; reply arrives on the thread |
| POST | /v1/me/checkin |
{ occurrenceId?: string, answers?: Record<string,string>, freeText?: string } |
{ accepted: true, runId } |
Optional structured reply |
| GET | /v1/me/goals / /v1/me/projects |
— | { items: Goal[] | Project[] } |
Owned or contributing |
| GET | /v1/me/growth-plan |
— | { plan: MemoryFact, history: MemoryFact[] } |
|
| PATCH | /v1/me/growth-plan |
{ content: string, reason: string } |
{ plan } |
Subject-editable |
| GET | /v1/me/feedback?cursor= |
— | { items: MemoryFact[] } |
feedback_given, feedback_received |
| POST | /v1/me/feedback/{factId}/response |
{ text: string } |
{ fact } |
Appends subjectResponse |
| GET | /v1/me/knows |
— | KnowsPage |
IC-4 |
| GET | /v1/me/knows/access-log?cursor= |
— | { items } |
|
| POST | /v1/me/knows/{factId}/redact |
{ reason?: string } |
{ status: 'redacted' | 'decision_opened', decisionId? } |
Non-deletable kinds open a decision to the MoR (EC-MGR-006) |
| POST | /v1/me/escalate |
{ message: string, urgency: 'now' | 'today', to?: 'mor' | 'owner' | 'safety' } |
{ escalation: Escalation } |
Uncancellable; suppressible:false; delivered on every surface in order |
| GET | /v1/me/escalations |
— | { items: Escalation[] } |
|
| POST | /v1/me/requests |
{ kind: 'pto' | 'expense' | 'policy_question' | 'document' | 'other', payload: object } |
{ request, runId } |
Quiet HR (IC-6); HRIS/finance remain systems of record |
| GET | /v1/me/requests/{id} |
— | { request, status, decisionId? } |
|
| GET | /v1/me/decisions?cursor= |
— | { items: Decision[] } |
Decisions about me; packets show existence and status only |
| GET | /v1/me/receipts/{receiptId} |
— | Receipt |
Only receipts whose visibility includes self |
| GET | /v1/me/shifts?from=&to= |
— | { items: Shift[] } |
Atoms |
| POST | /v1/me/shifts/{shiftId}/swap-request |
{ withPersonId?: PersonId, note?: string } |
{ runId, decisionId? } |
Atoms; in-policy swaps AUTONOMOUS |
| POST | /v1/me/shifts/{shiftId}/callout |
{ reason?: string } |
{ runId, escalationId? } |
Atoms; callOutException lifts caps |
| GET | /v1/me/checklists/{occurrenceId} |
— | { items } |
Atoms |
| POST | /v1/me/checklists/{occurrenceId}/items/{itemId} |
{ done: boolean, note?: string, photoRef?: string } |
{ item } |
Atoms |
4.2 Owner Console — /v1/owner¶
AUTH: isOwner in the tenant, or a PortfolioGrant with sufficient scope. Human sessions only; API keys are rejected on decide.
| Method | Path | Body | → | Notes |
|---|---|---|---|---|
| GET | /v1/owner/direction |
— | { goals: Goal[], values: string[], envelopes: Policy[], hardLines: Policy[], priorities: string[], version } |
OW-1 |
| PUT | /v1/owner/direction |
{ …same, reason: string } with If-Match |
{ direction, changeId } |
Produces policy.changed / org.change.applied |
| GET | /v1/owner/health?level=company|org|goal|person&id= |
— | { node, health, reasons: SourceRef[], narrative: string, children: HealthNode[], receiptId } |
Drill in three clicks |
| GET | /v1/owner/updates?cursor= |
— | { items: OwnerUpdate[] } |
{ id, weekOf, text, videoUrl?, decisions: DecisionId[], receiptId } |
| GET | /v1/owner/updates/{id} |
— | OwnerUpdate |
OW-2 |
| GET | /v1/owner/approvals?status=proposed|all&cursor= |
— | { items: Array<Decision & { receipt: Receipt, packet?: Packet }> } |
OW-3; packets included with noRecommendation |
| GET | /v1/owner/approvals/{decisionId} |
— | Decision & { receipt, packet? } |
|
| POST | /v1/owner/approvals/{decisionId}/decide |
{ status: 'confirmed' | 'rejected', note?: string, edits?: object } or for packets { humanDecision: string } |
{ decision, runId } |
Creates HUMAN_DECISION trigger; edits count as edited for graduation stats |
| GET | /v1/owner/escalations?status= |
— | { items: Escalation[] } |
|
| POST | /v1/owner/escalations/{id}/acknowledge / /resolve |
{ note?: string } |
{ escalation } |
|
| POST | /v1/owner/ask |
{ question: string } |
{ answer: string, receipt: Receipt, runId } |
OW-4; runs Chonksworth custom with read-only tools |
| GET | /v1/owner/decisions?since=&cursor= |
— | { items: Decision[] } |
Decision log |
| GET | /v1/owner/portfolio |
— | { tenants: Array<{ tenantId, name, health, openApprovals, escalations, promise: object }> } |
OW-5; fans out under grants |
| GET | /v1/owner/graduation |
— | { items: Graduation[] } |
Read; changes via admin |
4.3 GraphQL — /v1/graphql¶
Read-only, for console drill-down and ask-anything citations. Mutations are REST.
type Query {
orgTree(rootId: ID): OrgNode!
health(level: HealthLevel!, id: ID!): HealthNode!
goal(id: ID!): Goal
project(id: ID!): Project
person(id: ID!): PersonSummary # owner-safe projection: name, role, org, goals, health; never memory
decisions(filter: DecisionFilter, first: Int, after: String): DecisionConnection!
escalations(status: EscalationStatus, first: Int, after: String): EscalationConnection!
receipt(id: ID!): Receipt
narrative(orgId: ID!, weeks: Int = 4): [Narrative!]!
cadences(orgId: ID): [Cadence!]!
graduation: [Graduation!]!
}
type OrgNode { org: Org!, managerChonk: ChonkRef!, managerOfRecord: PersonSummary!, members: [PersonSummary!]!, children: [OrgNode!]!, goals: [Goal!]!, health: HealthNode! }
type HealthNode { id: ID!, kind: String!, health: Health!, reason: String!, asOf: DateTime!, sources: [SourceRef!]!, children: [HealthNode!]!, narrative: String }
enum HealthLevel { COMPANY ORG GOAL PROJECT PERSON }
enum Health { GREEN YELLOW RED UNKNOWN }
type Receipt { id: ID!, sources: [SourceRef!]!, ruleEvaluations: [RuleEvaluation!]!, rationale: String!, confidence: Float!, gaps: [String!]! }
PersonSummary is the only person type exposed to the owner through GraphQL; memory facts are not reachable from it (symmetry is served through owner_report to the person, not by exposing memory to the owner).
4.4 Admin — /v1/admin¶
AUTH: tenant admin role, or API key with matching scope. Every mutation requires reason.
| Area | Method | Path | Body / → | Notes |
|---|---|---|---|---|
| Tenant | GET / PATCH | /v1/admin/tenant |
Tenant / { patch, reason } |
Mode, atoms, retention, plan (plan is Sea Suite-ops only) |
| POST | /v1/admin/tenant/pause / /resume |
{ reason } |
Emits tenant.paused/resumed; escalations still deliver while paused |
|
| POST | /v1/admin/tenant/listening/end |
{ reason } |
Not before day 5; owner session only | |
| Onboarding | POST | /v1/admin/onboarding/draft |
{ providers: ConnectorProvider[] } → { runId } |
Starts onboarding_draft run |
| GET | /v1/admin/onboarding/draft |
→ { status, nodes, edges, proposals, gaps } |
||
| POST | /v1/admin/onboarding/draft/approve |
{ corrections: ChangeOp[], reason } → { changeId } |
Emits listening.started |
|
| Org Model | GET | /v1/admin/org/nodes?kind=&status=&asOf=&cursor= |
→ { items: NodeBase[] } |
asOf = version or timestamp |
| GET | /v1/admin/org/nodes/{id}?asOf= |
→ node | ||
| GET | /v1/admin/org/edges?type=&from=&to=&asOf= |
→ { items: Edge[] } |
||
| POST | /v1/admin/org/changes |
{ ops: ChangeOp[], reason, reasonCode? } with If-Match → Change |
Atomic; E_ORG_INVARIANT_* on violation |
|
| GET | /v1/admin/org/changes?since=&cursor= |
→ { items: Change[] } |
||
| GET / POST | /v1/admin/org/snapshots |
→ { items } / { reason } → { snapshotId } |
||
| GET | /v1/admin/org/export?asOf= |
→ JSONL stream | Portability | |
| GET | /v1/admin/org/merge-proposals?status= |
→ { items: MergeProposal[] } |
||
| POST | /v1/admin/org/merge-proposals/{id}/decide |
{ accept: boolean, canonical?: PersonId, reason } |
||
| POST | /v1/admin/org/people/{id}/offboard |
{ endDate, reason, hrisRef? } → { checklist } |
Rejected with E_ORG_INVARIANT_I-ORG-01 if MoR without replacement |
|
| POST | /v1/admin/org/people/{id}/unmerge |
{ reason } |
||
| Roster | GET / PUT | /v1/admin/roster |
{ chonks: Array<{ chonkId, enabled, orgIds, displayName?, species? }> } |
managed_by edges are derived from this |
| PUT | /v1/admin/roster/{chonkId}/voice |
Partial<VoiceProfile> + reason |
Charter and allowlist are not editable here | |
| Policies | GET / POST | /v1/admin/policies |
→ { items: Policy[] } / Policy + appliesTo + reason |
Hard lines require owner session |
| PATCH / DELETE | /v1/admin/policies/{id} |
{ patch, reason } / { reason } |
Delete = end with validTo |
|
| POST | /v1/admin/policies/evaluate |
{ action: ProposedAction, asOf? } → Verdict |
Dry run | |
| Graduation | GET | /v1/admin/graduation |
→ { items: Graduation[] } |
|
| POST | /v1/admin/graduation/{fn} |
{ state, ownerCap?, reason } |
Widening requires owner session; narrowing any admin | |
| GET | /v1/admin/shadow-log?fn=&cursor= |
→ { items: ShadowLogEntry[] } |
||
| POST | /v1/admin/shadow-log/{id}/review |
{ verdict, note? } |
Feeds graduation criteria | |
| Integrations | GET | /v1/admin/integrations |
→ { items: Integration[] } |
|
| POST | /v1/admin/integrations/{provider}/connect |
{ scopes?: string[] } → { authorizeUrl } |
OAuth start; callback at /v1/admin/integrations/{provider}/callback |
|
| DELETE | /v1/admin/integrations/{provider} |
{ reason } |
Tokens destroyed; mirrors retained per retention | |
| POST | /v1/admin/integrations/{provider}/resync |
— → { runId } |
||
| GET | /v1/admin/integrations/{provider}/health |
→ Integration['health'] |
||
| Cadences | GET / POST | /v1/admin/cadences |
→ { items } / Cadence + reason |
|
| PATCH | /v1/admin/cadences/{id} |
{ patch, reason } |
||
| GET | /v1/admin/cadences/{id}/occurrences?from=&to= |
→ { items: Array<{ fireAt, participantId?, runId?, status: 'satisfied' | 'missed' | 'degraded' | 'pending' }> } |
||
| Runs | GET | /v1/admin/runs?state=&chonk=&kind=&subject=&since=&cursor= |
→ { items: Run[] } |
|
| GET | /v1/admin/runs/{id} / /events / /receipts |
→ Run / { items: Event[] } / { items: Receipt[] } |
||
| POST | /v1/admin/runs/{id}/cancel |
{ reason } |
Decisions stay open | |
| POST | /v1/admin/runs/{id}/replay |
{ mode: 'VERIFY' | 'SHADOW_REEXEC' | 'LIVE_REEXEC', reason } → { runId } |
LIVE_REEXEC requires owner or Sea Suite ops session |
|
| POST | /v1/admin/runs |
{ chonkId, objective, subjectRefs, clientNonce } → { runId } |
MANUAL trigger |
|
| Decisions | GET | /v1/admin/decisions?status=&kind=&cursor= / /{id} |
→ Decision[] / Decision |
Packets: metadata only unless routed to the caller |
| Escalations | GET | /v1/admin/escalations?status= |
→ Escalation[] |
|
| PUT | /v1/admin/escalations/contacts |
{ safetyContactId: PersonId, opsContact?: string, reason } |
||
| Memory | GET / PUT | /v1/admin/memory/retention |
Tenant['retention'] + reason |
|
| POST | /v1/admin/memory/purge |
{ subjectId, kinds?: MemoryFactKind[], reason, legalBasis?: string } → { purged: number, auditId } |
Owner or admin session; audited with hashes | |
| Audit | GET | /v1/admin/audit?since=&action=&cursor= |
→ { items: AuditEntry[] } |
|
| POST | /v1/admin/audit/export |
{ from, to } → AuditExport |
Async; audit.export.ready |
|
| GET | /v1/admin/audit/exports/{id} |
→ AuditExport |
||
| GET | /v1/admin/audit/verify?from=&to= |
→ { ok: boolean, brokenAtSeq?: number, head: string } |
||
| Webhooks | GET / POST | /v1/admin/webhooks |
→ { items } / { url, events: string[], secret?: string } → { webhook, secret } |
Secret shown once |
| DELETE / POST | /v1/admin/webhooks/{id} / /test |
{ reason } / — |
||
| Budgets | GET / PUT | /v1/admin/budgets |
{ perRunKind: Record<RunKind, Partial<RunBudget>>, outreach: Policy } + reason |
Within tier caps |
| Simulation | POST | /v1/admin/sim/seed |
{ org: 'lanternfish' | 'harbortable', clock?: ISODateTime } |
Non-production cells only |
5. Webhook contracts¶
5.1 Inbound (integrations → ingress)¶
Endpoint pattern: POST https://ingress.{cell}.seasuite.app/v1/hooks/{provider}/{integrationId}. Ingress verifies the provider signature, records integration.event.received with payloadHash, returns 2xx within 2 s (work is asynchronous), and returns 2xx for duplicate deliveries (idempotent on deliveryId). Ingress never returns provider errors to the provider except 401 for a bad signature and 503 when the event log is unavailable (providers retry).
| Provider family | Verification | deliveryId |
Event types normalized to | Subject mapping | Default Chonk |
|---|---|---|---|---|---|
| GitHub | X-Hub-Signature-256 HMAC-SHA256 |
X-GitHub-Delivery |
work.pr.opened/updated/review_requested/reviewed/merged/closed, work.issue.*, work.deploy.* |
Repo → Org via externalRefs; author/reviewer → Person via identity github |
build |
| Linear / Jira / Asana | Linear Linear-Signature; Jira JWT/HMAC; Asana X-Hook-Secret |
Linear deliveryId; Jira webhookEvent + timestamp + issue id; Asana event id |
work.issue.created/updated/status_changed/assigned/blocked/unblocked/commented |
Project/issue → Project via externalRefs; assignee → Person |
build (eng), porpoise (product), ops (other) |
| Notion / Google Drive | Notion signature; Drive channel token | Notion event id; Drive X-Goog-Message-Number + channel |
doc.updated/shared |
Doc → Project by link | porpoise |
| Slack / Teams | Slack signing secret (v0); Teams JWT | Slack event_id; Teams message id |
comms.message.received (only in manager threads and channels the Chonk is in), comms.reaction, comms.member_joined |
Slack user → Person; DM to bot → IC_MESSAGE |
Managing Chonk of the person's org |
| Twilio (SMS / voice) | X-Twilio-Signature |
MessageSid / CallSid |
comms.sms.received, comms.call.received, comms.call.transcript |
Phone → Person (phoneE164) |
human (thread), ops (call-outs by intent) |
| Gmail / Calendar / Outlook | Google push channel token; Graph validation token | history id / message number | calendar.event.created/updated/cancelled, mail.received (only for connected manager mailbox) |
Attendee → Person | ops |
| Gusto / Rippling / Deel / BambooHR / Workday | Provider HMAC | Provider event id | people.hired, people.terminated (recorded, never decided), people.leave.started/ended, people.role_changed, people.pto.requested/approved, people.timesheet.* |
HRIS employee id → Person (anchor identity) | human; org placement proposals to admin |
| QuickBooks / Xero / Ramp / Brex / Stripe | Provider HMAC | Provider event id | finance.transaction.created, finance.request.created (Ramp/Brex approvals), finance.invoice.*, finance.subscription.* |
Card holder → Person; category → envelope policy | fin |
| HubSpot / Intercom | HMAC | Event id | crm.deal.stage_changed, crm.deal.updated, support.conversation.* |
Owner → Person; deal → Project (optional) | revenue, ops (support) |
| Square / Toast / Shopify POS | HMAC | Event id | pos.sales.summary, pos.forecast.updated, pos.86.updated, pos.reservation.* |
Location → Org(location) | ops |
| Deputy / When I Work / 7shifts / Homebase / time clocks | HMAC or polling | Event id or hash(objectId, updatedAt) |
shift.published/changed/uncovered, shift.callout, shift.swap.requested, timeclock.punch.in/out, timeclock.overtime_risk |
Employee → Person; location → Org | ops; security for compliance |
| IoT / sensors | Shared secret per feed | Sequence number | iot.reading, iot.alert (temperature, door, equipment) |
Device → Org(location) | security |
| MCP agent workers | mTLS | Request id | agent.task.completed/failed/needs_input |
Agent → Person(agent worker) | Managing Chonk |
Normalized shape:
interface InboundWebhookEnvelope {
provider: ConnectorProvider;
integrationId: IntegrationId;
deliveryId: string;
receivedAt: ISODateTime;
signatureVerified: true;
eventType: string; // normalized name above
occurredAt?: ISODateTime;
subjectRefs: NodeRef[]; // resolved; unresolved identities produce a MergeProposal or an 'invited' Person
externalRefs: ExternalRef[];
normalized: Record<string, unknown>; // provider-specific fields mapped per doc 11
payloadHash: string; // raw payload stored encrypted for 30 days for replay/debug
}
5.2 Outbound (Sea Suite → customer)¶
Customers subscribe via /v1/admin/webhooks to any event marked ext in §3.
interface OutboundWebhook {
id: string; // evt_… (the source event id; idempotency key for the receiver)
type: string; // event type
tenantId: TenantId;
seq: number;
ts: ISODateTime;
schemaVersion: string;
data: unknown; // the event payload, minus memory content and packet contents
}
Headers: X-Sea-Event: <type>, X-Sea-Delivery: <uuid>, X-Sea-Signature: t=<unix>,v1=<hex HMAC-SHA256(secret, t + '.' + rawBody)>. Receivers must verify v1 and reject t older than 5 minutes. Delivery: 2xx within 10 s counts as success; otherwise retries with exponential backoff (1 min, 5, 30, 2 h, 6 h, 24 h) then dead; a webhook with 50 consecutive failures is disabled with an admin notification. Ordering across retries is not guaranteed; receivers order by seq. Payloads never include MemoryFact.content, Packet.contents, or receipts with visibility='self'.
6. Error codes¶
| Code | HTTP | Meaning | Retry |
|---|---|---|---|
E_AUTH_REQUIRED |
401 | No credentials | No |
E_AUTH_INVALID |
401 | Expired or bad token; bad webhook signature (E_WEBHOOK_SIGNATURE alias) |
No |
E_FORBIDDEN |
403 | Principal lacks role or scope | No |
E_TENANT_MISMATCH |
403 | X-Tenant-Id does not match the token and no grant covers it |
No |
E_HUMAN_SESSION_REQUIRED |
403 | API key attempted to decide a PROPOSE_CONFIRM decision, record a HUMAN_ONLY packet, widen graduation, end listening, or run LIVE_REEXEC |
No |
E_NOT_FOUND |
404 | Unknown id or not visible to the principal | No |
E_VALIDATION |
400 | Schema or prefix error; details.fields[] |
No (fix input) |
E_REASON_REQUIRED |
400 | Mutation without reason |
No |
E_IDEMPOTENCY_CONFLICT |
409 | Same Idempotency-Key, different body |
No |
E_ORG_VERSION_CONFLICT |
409 | If-Match version stale |
Yes, after re-read |
E_ORG_INVARIANT_<ID> |
422 | Change would violate invariant I-ORG-<ID>; details.invariant, details.nodes[] |
No |
E_ORG_MERGE_KIND_MISMATCH |
422 | Merge between human and agent | No |
E_ORG_CYCLE |
422 | Alias of E_ORG_INVARIANT_I-ORG-03 with the cycle path in details.path |
No |
E_POLICY_BLOCKED |
422 | Hard line or policy forbids the request; details.policyIds, details.evaluations |
No |
E_AUTONOMY_HUMAN_ONLY |
422 | Request would execute a human-only action; a packet route is returned in details.packetId |
No |
E_DECISION_TERMINAL |
409 | Decision already decided/expired | No |
E_DECISION_STALE |
409 | Confirmed, but re-classification changed the verdict; details.newVerdict |
No; a new proposal follows |
E_ESCALATION_UNSUPPRESSIBLE |
422 | Attempt to cancel or delay an IC → human, safety, or cadence-missed escalation | No |
E_GRADUATION_CAP |
422 | Requested state above ownerCap or criteria unmet; details.criteria |
No |
E_LISTENING_MIN_DAYS |
422 | Listening cannot end before day 5 | No |
E_TENANT_PAUSED |
423 | Tenant paused; reads allowed, runs and writes refused | Yes, after resume |
E_RUN_STATE |
409 | Operation invalid for run state (cancel a SUCCEEDED run) | No |
E_RUN_BUDGET |
422 | Requested budget above tier cap | No |
E_MEMORY_FORBIDDEN_CONTENT |
422 | Write matched a forbidden pattern (§5.4 of doc 04); details.pattern |
No |
E_MEMORY_NOT_REDACTABLE |
409 | Kind is annotate-only; response includes decisionId opened for the MoR |
No |
E_INTEGRATION_NEEDS_REAUTH |
422 | Connector token revoked; details.authorizeUrl |
After re-auth |
E_INTEGRATION_UNAVAILABLE |
502 | Provider down for a synchronous call (OAuth start, resync) | Yes |
E_RATE_LIMITED |
429 | Per-key limit; Retry-After |
Yes |
E_BACKPRESSURE |
503 | Cell or tenant at L3; Retry-After |
Yes |
E_EXPORT_IN_PROGRESS |
409 | An audit or Org Model export is already running for the range | Yes, later |
E_SIM_ONLY |
403 | Endpoint available only in non-production cells | No |
E_INTERNAL |
500 | Unexpected; requestId for support |
Yes, with backoff |
7. Edge cases¶
| ID | Scenario | Signal / detection | Rule | Action (autonomy class) | Escalation | Test |
|---|---|---|---|---|---|---|
| EC-API-001 | Automation API key calls POST /v1/owner/approvals/{id}/decide |
Principal kind api_key on a human-only route |
Approvals and packets require a human session | E_HUMAN_SESSION_REQUIRED; audit entry decision.human attempt (AUTONOMOUS) |
Admin notified on 3 attempts/day | Call with a key in sim; assert 403 and audit entry |
| EC-API-002 | GitHub delivers the same X-GitHub-Delivery twice, 3 days apart |
Idempotent on deliveryId |
Duplicate inbound deliveries are acknowledged, not processed | 200 with Idempotent-Replayed: true; no run (AUTONOMOUS) |
None | Re-post a recorded delivery |
| EC-API-003 | Owner PUT /v1/owner/direction with stale If-Match after Chonksworth's goal-draft change |
Version mismatch | Optimistic concurrency | E_ORG_VERSION_CONFLICT with details.currentVersion and a diff (AUTONOMOUS) |
None | Concurrent writes in sim |
| EC-API-004 | Customer webhook endpoint returns 200 but takes 25 s | Delivery timeout 10 s | Success requires 2xx within 10 s | Retry per schedule; receiver dedups on id (AUTONOMOUS) |
Admin after 50 consecutive failures | Slow endpoint in the webhook test harness |
| EC-API-005 | POST /v1/me/escalate while the tenant is paused |
tenant.phase = PAUSED |
Escalation right survives pause | Delivered; escalation.opened (AUTONOMOUS, uncapped) |
MoR; owner if unacknowledged in 2 h | Pause tenant in sim; escalate; assert delivery |
| EC-API-006 | GET /v1/me/knows for a person merged last week |
Person status=merged |
Merged ids resolve to canonical | 307 to canonical person's page; access logged (AUTONOMOUS) | None | Merge then fetch |
| EC-API-007 | Admin POST /v1/admin/memory/purge for a person with open HUMAN_ONLY packet |
Packet references the subject | Packets are decision records, not memory; purge does not touch them | Memory purged; packet untouched; response lists what was not purged and why (AUTONOMOUS) | Owner informed | Purge in sim; assert packet intact |
| EC-API-008 | Inbound Slack event from a channel the Chonk is not a member of (Slack sends member_joined for a private channel) |
Channel not in the Chonk's membership list | Never reads private channels | Event dropped at ingress with integration.event.dropped{reason:'not_in_scope'}; nothing stored beyond the hash (AUTONOMOUS) |
None | Send a synthetic private-channel event |
| EC-API-009 | POST /v1/admin/graduation/planning to AUTONOMOUS by an admin who is not the owner |
Widening requires owner session | Widening is direction | E_HUMAN_SESSION_REQUIRED variant details.requires='owner' |
None | Call as admin in sim |
| EC-API-010 | Outbound webhook subscribed to decision.* receives a human_only_packet decision |
Payload filter | Packet contents never leave | data.decision with summary replaced by "[human-only packet]" and no receipt (AUTONOMOUS) |
None | Subscribe and create a packet in sim; assert redaction |
8. Risk control¶
RC-API-01 — Data leaving the tenant boundary through APIs or webhooks. Owner: Chief Security Officer. Metric: responses containing memory content or packet contents to a principal without visibility (target 0); webhook payload filter test failures (target 0).
- Detect: response filters are typed (serializers per principal kind); a canary memory fact per tenant with a unique token is asserted never to appear in owner, admin, or webhook responses by a continuous synthetic probe.
- Contain: on a probe hit, the affected endpoint is disabled by feature flag within the cell; webhooks for the tenant pause.
- Correct: serializer fixed; affected deliveries listed from the audit log and the tenant notified per Security, Privacy, Compliance.
- Learn: the response shape joins the contract test suite that diffs every endpoint's serializer against the visibility matrix on every deploy.
Open questions¶
- GraphQL surface size. Whether ask-anything should expose a
personquery at all, or only org-level nodes with people as unlinked names; current answer isPersonSummarywith no memory reachability. - Webhook payload minimization. Some customers will want
decision.*payloads with receipts for their own audit; that means receipts crossing the boundary. Consider areceipts: 'none' | 'owner_visible'option per webhook. - API keys for MoR actions. A manager of record may want to acknowledge escalations from their own tooling; today acknowledgment requires a human session. Decide whether acknowledgment (not decision) can be key-scoped.
- Surface-authenticated writes. Slack interactive buttons (Approve/Reject) arrive as Slack-signed payloads, not human sessions; they must count as human decisions. Define the binding (Slack user → verified identity → session-equivalent) precisely in doc 13.
- Version negotiation for events.
schemaVersionon events is per event type; a consumer registry that declares supported versions would let us evolve payloads without breaking read models.