This page maps every term in the user glossary
onto the schema that actually backs it. It is the identifier-level companion to
the core data model, which draws the tenancy and resource spine;
here the unit is the term, not the table. Migration numbers refer to files in
server/migrations/ (see the migration index).
Naming is the ubiquitous language of
ADR 0040: one term, one meaning across
DB, code, API and UI.
The tenant table is workspaces, renamed from teams in migration 169
together with team_members → workspace_members, team_invites →
workspace_invites, and every team_id FK → workspace_id.
organizations.kind='team' stays an internal enum value; its user-facing
label is Organization (org_kind_team), against Personal for
kind='personal'. Never surface the raw value.
team/teams is otherwise reserved for the agent domain (agent_teams,
max_teammates). An architecture test fails a new team_id column or a new
team-named tenant type, and a frontend i18n lint fails a new user-facing
“Team” string outside the agent and delivery_* namespaces.
kind CHECK is personal or team, NOT NULL DEFAULT team (mig 140). The plan is per organization on subscriptions, whose PK was re-keyed from workspace_id to organization_id in mig 143 — never look for a plan on a workspace.
UNIQUE (user_id) enforces the single-org model (no switcher, no X-Organization-Id). role is the wider org set: owner, billing_admin, admin, member.
Workspace
workspaces; FK workspace_id on ~34 tables
Renamed from teams in mig 169 (catalog-only rename, no row rewrite). Also carries autonomy_level, autonomy_overrides, experience_tier.
Workspace membership
workspace_members — workspace_id, user_id, role
role CHECK tightened to owner, admin, member in mig 245. Trigger trg_team_member_org_invariant — deliberately not renamed by 169, so unchanged RLS policies still resolve — requires a matching organization_members row for the workspace’s org; a violation raises check_violation.
Table created in mig 144, which also added the FK (ON DELETE SET NULL, so archiving a seat type unassigns rather than blocks). Entitlements resolve first-non-null: user override › seat type › workspace override › org plan. Billing quantity lives on subscription_items (organization_id, seat_type_id).
Project
projects
PR-flow autodev config no longer lives here: mig 299 dropped 12 auto_developer_* columns (only auto_developer_workflow_id survives), 302 dropped the auto_issue_debate_* set, 297 dropped the visual-verify columns. The pr_review row of project_automations is the authority. backlog_merge_policy CHECK is pr_only, auto_on_green, full_auto (mig 287).
config is the per-launch override bag (effort, harness knobs, teammates, closeout); agent_profile_id is the WHO authority. The status/branch/timestamp columns are a denormalised cache of the latest run, never the source of truth.
next_event_seq (mig 220) mints a gapless, monotonic events.sequence_num per run via an atomic UPDATE … RETURNING; uq_events_run_seq is the safety net. autonomy_policy_json (mig 214) is the policy frozen at launch. events.task_id is nullable since mig 331 so a workflow-node event can key on a run with no task.
agent_profile_id added in mig 293 (ON DELETE SET NULL). cadence CHECK is once, daily, weekly, monthly, interval since mig 323, which also added interval_minutes bound by CHECK ((cadence = 'interval') = (interval_minutes IS NOT NULL)) and a 5–1440 range — cadence and column are inseparable.
Workflow
workflows + workflow_nodes + workflow_edges
workflow_nodes is UNIQUE (workflow_id, node_key); edges join by the node_keytext (source_node/target_node), with source_handle defaulting to out. workflows.stages JSONB arrived in mig 311.
execution_mode CHECK is normal or workflow since mig 285; ultracode was retired to the effort axis (minimal…max), though the runtime still tolerates a legacy in-flight value in task config. is_workspace_default (mig 119) is bounded by the partial unique index uq_agent_profiles_workspace_default — at most one default per workspace.
Teammates
Not a column.tasks.config keys agent_teams (bool) and max_teammates (int)
No table and no migration; ADR 0040 keeps the key name because it is Claude Code’s own. The runtime emits CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS / CLAUDE_CODE_MAX_TEAMMATES for claude, and AGENT_TEAMS=1 for codex/gemini. max_teammates is emitted only when agent_teams is true.
Skill
agent_capabilities with kind='skill' — Markdown in config.instructions
kind CHECK is mcp or skill; UNIQUE (workspace_id, kind, name). config also carries targets, the per-harness allowlist. An induced skill is staged in the separate skill_proposals table (mig 218) and only becomes an agent_capabilities row once published. Capability writes are workspace-admin gated.
Council
councils + council_participants + council_turns
mode CHECK is analyze, propose, review, standup — the UI labels differ from these stored values, so map rather than assume. target_type gained workflow_step in mig 314, which is how a pipeline stage runs a council. The synthesis lands on judge_profile_id / judge_synthesis / judge_confidence.
scope CHECK is personal, workspace, project. The source_type CHECK is deliberately wider than the user-facing allowlist (it admits repo_sync, agent_experiment and a harness_memory:% arm); validate_memory_source_type is the narrow gate, and system paths bypass it via create_system_memory. Governance is two-tier: workspace_settings.memory_governance_policy (mig 249) with a project override projects.memory_governance_policy (mig 289).
MCP
No table. The catalog is code, exported to docs/mcp/ and drift-gated
Per-task/workspace/profile caps are derived from audit_events so they survive restarts; in-flight reservations live in mcp_cap_reservations, whose task_id is nullable since mig 268 for the task-less external-gateway principal.
NULL means inherit, so the chain profile › project › workspace always terminates at a concrete workspace value. The result is then clamped by the plan ceiling plans.features->'autonomy.level_max' and the onboarding cap; a per-arm project_automations.autonomy_override (mig 210) can only lower it. runs.autonomy_policy_json freezes the outcome at launch.
kind CHECK is tool, question, visual, tier_change (widened by mig 321). tier_change is deliberately notquestion, because the parent-answer guard admits only question — a machine must not self-approve the gate it raised. Partial index uq_task_tool_approvals_task_pending allows at most one pending approval per task, and hold-policy rows are never auto-swept.
Backlog
backlog_items plus the dispatch arm project_automations with kind='backlog'
project_automations.kind CHECK is now pr_review, intake, backlog: tracker was renamed to intake (mig 303) and debate was retired (mig 291). Issue import and dispatch are separate paths — import runs regardless of mode.
Backlog item
backlog_items — source, external_id, scope, state
Dedupe key is UNIQUE (project_id, source, external_id): mig 303 added source and backfilled it from tracker, mig 304 dropped the old column and its unique index. scope is nullable with CHECK small, medium, large (mig 226); NULL is routed as neutral. convergence_round / convergence_cost_usd bound the review loop (mig 232), and closed_at carries CHECK (closed_at IS NULL OR state = 'done') (mig 310).
The approval_mode column was dropped in mig 301 after mig 300 folded it: concept → stages.plan.approval, pr_draft → stages.implement.draft_pr_approval, human_gate → projects.backlog_merge_policy = 'pr_only'. concept_gate_on reads the flag with no enum fallback left, and the gate applies only to a kind='backlog' arm.
The two booleans span all four outcomes, including the previously inexpressible silent close (false, true). The legacy completion_action column still exists (close, comment, nothing) as a lossy dual-read fallback for a row whose boolean pair is NULL; its drop is deferred. Resolve the pair first, the enum only as fallback.
Closeout
workspace_settings.closeout_config (JSONB NOT NULL, full default) + projects.closeout_config (nullable, sparse) + task.config.closeout; signals on task_outcomes.closeout_signals — mig 283
Resolution is per step, first-non-null: task ?? project ?? workspace ?? built-in. There is deliberately no master switch; the fold lives once in sc-persistence::models::closeout and is shared by the memory gate, the env injection and the status signal.
apps — UNIQUE (workspace_id, name), manifest_yaml; source tree in app_files (mig 107)
apps.db_namespace_id (mig 279) is the App’s database authority; app_db_resource_id survives only as the hint for which physical Postgres. app_migrations is no longer a table — mig 280 folded it onto db_namespace_migrations and left a read-only compatibility view. The schema is app_<id>, and db_namespace_bindings enforces exactly one binder.
Script
scripts (mig 066) locally; the catalog row is marketplace_items with kind='script'
An installed script is tracked on marketplace_installs.installed_script_ids. The Wasm script substrate is pure-compute and DB-isolated by design; it reaches the network only through a bound resource’s allowlisted authority.
Connector
marketplace_items with kind='node', plus the per-workspace registry workflow_node_types
workflow_node_types is UNIQUE (workspace_id, slug) and carries preset_config, required_resource_kinds, source_item_version_id. The component bytes ride the version manifest as component_data_b64.
kind is a CHECK list, currently smtp, telegram_bot, webhook_endpoint, imap, postgresql, s3, discord, seafile_webdav, fints_bank, chatwoot, intercom, custom (last widened by mig 324). Each kind gates on the plan feature key resources.<kind>. secret_vault_path is nullable, so a secret-less resource is representable. The UI calls these Connections.
status CHECK is pending, online, offline, disabled; drain_state CHECK is active, cordoned, draining (mig 234). Dispatch reads status='online' AND drain_state='active'. lease_epoch is not on runners — it is on runner_work_assignments / connector_run_assignments (mig 239), because fencing belongs to the assignment. Enrollment lives in runner_enrollments + runner_grants (mig 236).
marketplace_items.kind CHECK is app, script, bundle, runner_offer, workflow, node — six values, so do not omit runner_offer. marketplace_installs is UNIQUE (workspace_id, item_id) since mig 112: a re-install upserts, never duplicates. install_manifest_sha256 drives drift detection.
Bundle
marketplace_items with kind='bundle'; contents ride marketplace_item_versions.package_manifest JSONB under members
members is a manifest block, not a column — a bundle is folded at publish into one members block plus a unioned install spec and requirements, so one install is one marketplace_installs row. Publish rejects fewer than two members, a member-slug collision, a self-reference, a cycle, more than one member db_namespace, and a paid member in a free bundle. An App member may carry its tree inline instead of a source_tarball_url / source_tarball_sha256 pair.
No table. Per-surface session state in sc-iface-telegram/src/live_mode and sc-iface-discord/src/live_mode, and the web follow engine
Follow is per surface: a Telegram launch streams only in Telegram. Both chat surfaces share the same pure render substrate; only the transport differs.
Both nullable with CHECK reduced or chatty. NULL is the layering signal, not a value: per-account ?? user default ?? built-in reduced. Orthogonal to debug mode — two axes, one renderer, one redaction wall.
Debug mode
workspace_settings.allow_debug BOOLEAN NOT NULL DEFAULT false (mig 254)
Fail-closed by default, unlike onboarding_mode, which coalesces to TRUE. The per-session activation on top of this ceiling is ephemeral and never persisted; the ceiling is enforced server-side on every surface.
Delivery board
Frontend route /reports/delivery; HTTP surface GET /api/team-board; Rust services::team_board and sc-persistence::queries::delivery_board
The UI, i18n (delivery_*) and route say delivery; the HTTP path and Rust module still say team_board. That split is intentional and unrenamed — there is no /delivery backend route. Only one i18n key is literally delivery_board_*.