Skip to main content

Storage map

Synced from bicycle-studio-api

Source: docs/architecture/storage.md at origin/platform (c2ce9cf), synced 2026-09-26. Do not edit this page here; change the source and run yarn sync:studio.

Every kind of state Studio and its runtime keep, where it lives, who writes and reads it, how durable it is, and what a restart loses. Verified against Studio API a78212f, data-app-runtime b21a421, studio-snapshot-worker 2241304, agent-service 95c607eb4 and the preview ConfigMaps.

Backends​

BackendWhereDurabilityNotes
blob-store documentsblob-store.plat-svc:80; Studio metadata namespace bicycle-studio-meta (BSA_METADATA_NAMESPACE), data namespace data-apps (BSA_BLOB_NAMESPACE), runtime namespace rtDurable (object storage)blob-store prefixes every key with the tenant derived from the caller's credential, so a document path below means {tenant}/{namespace}/{path}. BSA_METADATA_STORE=blob on preview and prod; the sql option (SQLite / Postgres table workflow_docs and app tables) exists for laptops. create is read-then-write-then-read-back, not atomic; record writes that must not race use a CAS token (workflow.json) or create-only keys
Redisplat-svc-redis.plat-svc:6379, Redis 7.4.8, one replica, AOF on, allkeys-lru, no authTesting-grade (#18): keys can be evicted under memory pressureAll Studio broker keys are prefixed bsa-rt:, claim keys bsa-claim:, agent runs / analysis / app cache studio:. When Redis is unreachable the broker, agent runs and analysis are switched off at startup
Postgres (Cloud SQL, private IP)database workflow-runner (workflow-runner; app role workflow_runner_app, RLS by app.tenant_id), database agent-service (usage ledger)DurableStudio has no Postgres yet. The runtime's rt schema (rt.claims, rt.invocations, rt.kv) is DDL only; the Postgres store backend is a stub in bicycle-exec and interface-only in Studio
Temporal<temporal host>, namespace defaultDurable workflow historiesTask queues wf-control[-preview], wf-io[-preview] (runner), data-app-snapshot[-preview] (snapshot worker)
Pod-local disk / memoryruntime-server invocation store (memory backend on preview and prod), agent-service run files under its scratch dir, Studio in-memory tasks and cachesLost on restartListed per row below

Functions​

StateWhereWriter → readersDurability / retentionRestart loses
Function record (name, kind, manifest incl. an agent function's manifest.agent.spec, disabled, enabled_state)functions/{tenant}/{name}/function.json (tenant library); app-scoped: functions/{tenant}/_app/{app_id}/{name}/… and …/sha/{sha12}.jsonStudio FunctionService → Studio, MCP tools/listDurable, no TTL; disable is a soft state on the record (#44)—
Draft (version 0) and draft revisions…/draft.json, …/draft_revs/{revision:08d}.json (create-only)Studio (editor saves, heartbeats) → StudioDurable; 409 draft_conflict on a stale revisionediting presence is not stored beyond the revision
Versions…/versions/{n}.json (immutable)Studio publish → broker resolve, MCP pinsDurable—
Packages (code zips)data ns functions/{tenant}/packages/{sha256}.bin; runtime-server fetches {namespace}/functions/{tenant}/{key} with the caller's credentialStudio upload → runtime-server (local digest-keyed cache, ≤ 256 entries)Durable; ≤ 8 MiB (BSA_FUNCTIONS_MAX_PACKAGE_BYTES)runtime-server's local package cache
Publish reviews…/reviews/{diff_sha256}.json {revision, shown_to, shown_at}review GET → publish POSTDurable; valid BSA_FUNCTIONS_REVIEW_TTL_SECONDS (900 s) for that person—
Tenant / app indexfunctions/{tenant}/_index/tenant.json, _index/app_{app_id}.jsonStudio on every save/publish → list routesDurable; rebuilt from records if lost—
Badges (tests / evals)evals/{tenant}/[_app/{app_id}/]_badges.jsoneval and test runs → function list and headerDurable—
Evalsevals/{tenant}/{fn}/{datasets/{evs_id},policy,runs/{evr_id},proposals/{evp_id}}.json, evals/{tenant}/_runs/{evr_id}.json, _quota/{yyyy-mm-dd}.jsonStudio evals serviceDurable; a running eval is an in-process task: runner_lost after BSA_EVALS_RUN_STALE_SECONDS (900)in-flight eval runs

There is no separate agents store: an "agent" is a function of kind agent; agent limits per tenant are the document settings/{tenant}/agent_limits.json (staff-written).

Workflows (Studio side, BlobDocBackend(root="wf"))​

StateWhereWriter → readersDurability / retentionRestart loses
Record (title, slug, state, published_revision, draft, notify, expose, schedules, schedule_overrides, disabled)wf/{tenant}/{wf}/workflow.json (CAS field cas); slug index wf/{tenant}/_slugs/{slug}.json; tenant index wf/{tenant}/_index/workflows.jsonStudio WorkflowService → Studio, MCPDurable, no TTL—
Revisions (document, files list, lock, validation)wf/{tenant}/{wf}/revisions/{n}.json (immutable, create-only); files by content at data ns wf/{tenant}/{wf}/files/{sha256}.binStudio patch/put_file/publish → Studio; the runner reads bundle files at {tenant}/data-apps/wf/{tenant}/{wf}/files/{sha256}.binDurable—
DraftThe head revision (revision.kind: draft); editing[] presence on the record for 120 s after a heartbeatStudioDurable—
Runs (Studio's projection)wf/{tenant}/{wf}/runs/{run_id}.json, run-head index wf/{tenant}/{wf}/_runs.json; grant → run index wf/{tenant}/{wf}/grants/{jti}.jsonRemoteRunner start, the events relay, reads that refresh status from the runnerDurable, no TTL. Runs before WP2 have invocation_id: nullin-process (deprecated path) runs: a dead pod leaves running, the next read marks interrupted
SchedulesOn the record (schedules[]: wft_{wf}_{trigger}_{seq}, sync_state, schedule_seq) and in scheduling-service; tick dedupe wf/{tenant}/{wf}/ticks/{trigger}/{fire_key}.json (create-only)publish / pause / resume / disable → scheduling-service; the tick routeDurable. An id is never reused (seq bumps on every create)—
Artifact versions, pointers, fingerprints (in-process path only)wf/{tenant}/{wf}/versions/{artifact}/{pkey}/{av_id}.json, pointers/{artifact}/{pkey}.json, fp/{artifact}/{pkey}/{fp}.json, bytes at data ns `data/{artifact}/{pkey}/{av_id}.json.png`the deprecated in-process runner; Studio also keeps the runner's snapshot PNGs here (rs_{run}_{node}_{attempt}.png)Durable
Approvals (Studio copy)wf/{tenant}/_approvals/{apr_id}.json (tenant inbox in one read); on the remote path the runner's approvals table is the truth and Studio mergesStudio decide → inboxDurable; expiry applied on read (≤ P14D)—
Ledger (in-process path)wf/{tenant}/{wf}/ledger/{action}/{key_hash}.json (create-only claim)deprecated in-process runnerDurable—
Mail policywf/{tenant}/_policy/mail.json `{allowed_domains, external: approvedenyallow, updated_by, updated_at}`tenant Admin PUT → validate/plan facts, sends
Action policy (landed a78212f)wf/{tenant}/_policy/actions.json {auto_allowed: [action ids], updated_by, updated_at}; deployment side BSA_WORKFLOW_ACTIONS_ALLOWED (default [])tenant Admin PUT → validate/plan facts, :send re-checkDurable—
Destinations (deprecated alias lists)wf/{tenant}/_destinations/{dst_id}.jsonAdmin PUT (Deprecation: true) → alias resolution for old revisions; destinations_migration moves them onto send stepsDurable—
Publish reviewswf/{tenant}/{wf}/reviews/{diff_sha256}.jsonreview GET → publish POST within REVIEW_TTL_SDurable—
Workflow blobs (function-node files, e.g. ML models)blob-store {tenant}/workflows/{wf}/store/{name} (declared in the document's blobs; the function asks for blob.read/blob.write on those names)a code function via the fn/1 blob capability (runtime-server gateway, scoped by the rt-invoke/1 grant) → later runs; the guide's pattern is model-<date>.skops + model-latest.jsonDurable, unversioned (put the version in the name)—

Workflow runs (workflow-runner Postgres, database workflow-runner)​

Migrations 0001–0004. All tenant tables carry tenant_id under row-level security. No retention or cleanup job exists: rows accumulate.

TableHoldsWriter → readers
revisionsevery revision Studio pushes (document, lock, files, doc_sha256, form, app_version, overlay_rev, label, timezone)Studio PUT …/revisions/{n} → the runner
workflow_seqplan_seq and state per workflowrunner
run_triggersone row per trigger (trigger_id, env, deploy_env, logical_date, idempotency_key, `type manualcron
runsthe run (status, `deploy_env prodpreview, plan, triggerincl.policy.on_success, cost_usd, cancel_requested, error, temporal_workflow_id, **run_grant`** in clear) — unique active run per (tenant, workflow, env, logical_date)
node_runsone per node partition (status, fingerprint, cache_hit, attempts, outputs, checks, llm, cost_usd, error, log)activities
attemptsexecutor attempts (executor, job_name, pod_uid, grant_scope, staging_prefix, exit_reason)activities
artifact_versionsversion metadata (data_key, rows, bytes, columns, checks, complete, missing, stale, withdrawn, training fields); bytes at blob {tenant}/rt/wf/{wf}/att/{attempt_id}/{artifact}.json (≤ 64 MiB, written once per attempt)run_partition → Studio artifact routes
pointers, pointer_moves, cache_index, leaseswhat serves per artifact and partition, the move audit, fingerprint cache, locksrunner
approvals, approval_decisionssend approvals (state, payload_hash, payload_data_key, preview, review, gaps, approvers, expires_at, context since 0004, receipt) and each person's decisionopen_action / decisions from Studio → the wf-action child workflow, Studio inbox
action_ledger, ledger_attempts, ledger_recipientsthe per-action ledger keyed by key_hash = sha256(rendered idempotency_key)[:32]; one open attempt per key (ledger_one_open); per-recipient state `approvedsending
run_indexone row per run for the federated run list (kind, parent_run_id, principal, deploy_env, status, cost_usd, links)runner → GET /v1/tenants/{t}/run-index
events_outboxthe run's events (type, payload, relayed_at) relayed at least once, ≤ 200 per run every 2 s, to Studio POST /api/internal/workflows/{wf}/runs/{run}/eventsactivities → relay task

Temporal holds the control flow only: workflow data-app-dag (id wfr:{tenant}:{run_id}, reject-duplicate) and the wf-action child per send (abandon-on-parent-close, waits for the decision signal or expiry). A runner restart loses only the in-flight activity's progress since its last DB write; the DB is the truth and activities are idempotent by it.

Invocations (Studio, root invocations)​

StateWhereWriter → readersDurability / retentionRestart loses
Invocation record (every kind: code, llm, agent, classify, lookup, workflow root and node children)Durable copy invocations/{tenant}/by-id/{inv}.json, written with the tenant service identity; live routing record Redis bsa-rt:inv:{id}broker submit / poll / relay → GET /invocations/{id}, /events, /traceRedis BSA_FUNCTIONS_RECORD_TTL_SECONDS (7 d); blob copy no TTL, best effort. After the Redis record expires, reads answer from the copy (terminal)—
Day indexinvocations/{tenant}/idx/{yyyymmdd}/{created}~{inv}~{kind}~{caller8}.json (empty bodies; a list walks the last 14 days)broker on write → GET /invocations, GET /runsDurable—
Events / tracePart of the record (events[], trace_count); code invocations pass runtime-server's events through; agent invocations read agent_events from the agent runbroker, relayAs the recordcode events on runtime-server: memory store (7 d TTL nominal; lost on its restart), 410 once drained
Result reuse (app calls)Redis bsa-rt:reuse:{tenant}:{key} (25 h) and bsa-rt:reuse-lock:… (20 s); durable invocations/{tenant}/reuse/{key}.json {latest, succeeded}broker invoke_runtime24 h window max—
Idempotency (service tokens)Redis claim `bsa-claim:rt-idem:{tenant}:{rtref}:{sha(key)}`, 24 hbrokerbest effort
Quotas and leasesRedis bsa-rt:conc:t:{tenant}, conc:r:{tenant}:{rt}, conc:f:{tenant}:{scope}:{name} (zsets, TTL timeout + 300 s); CU counters `bsa-rt:cu:trf:…:{yyyymmdd}(2 d);settled:{inv}, desc:{root}, fanout:{inv}, jti:{jti}:{seq}, llm-nllm-usd:{inv}; reaper zset reap:remote`
Agent queue and slotsRedis zset bsa-rt:aq:{tenant} (queued agent invocations), aq:alive:{inv} (45 s), aq:rr, aq:last:{tenant} (7 d), aq:gbusy (20 s), aq:try:{inv}:{attempt} (60 s); the tenant slot is the agent-runs zset studio:agent:{tenant}:slots (lease 900 s)broker _start_or_queue, dispatched by the caller's own pollsBSA_AGENT_RUNS_QUEUE_TTL_SECONDS 600 then failed: queue_timeout; depth ≤ 50queued records survive in the durable store and are re-indexed
Draft runsllm: inv_draft_{hex} recorded inline; agent: inv_agent_{run_id}; code/classify/lookup: ordinary inv_…DraftRunnerAs recordsin-flight :submit llm/lookup/classify tasks (RemoteKinds._tasks) fail deadline_exceeded via the reaper
MCP pins and cancelRedis `bsa-rt:mcp-pin:{tenant}:{subject}:{namewf:{slug}}(3600 s),mcp-call:{tenant}:{subject}:{request_id}`/mcp/functionsbest effort
runtime-server's own invocation storeinv:{id} (7 d), inv:by-tenant:{tenant} in the stores factory; memory backend on preview and prod (no BICYCLE_STORES_* set)runtime-server exec → GET /v1/invocations/{id}, /v1/tenants/{t}/run-indexnot durableall live code invocations on that replica

Agent runs (Studio agent_runs + agent-service)​

StateWhereWriter → readersDurability / retentionRestart loses
Studio run recordRedis studio:agent:{tenant}:run:{run_id} (24 h); indexes `…:by_finding:{app-}:{key}, …:by_subject:…(zsets, 30 d); idempotency lock…🔒{app-}:{idem}(15 min); daily budget…:budget:{yyyy-mm-dd}; slots …:slots` (lease 900 s; nested runs take none)AgentRunsService.submit → /agent-runs/{id}, invocations
Studio durable copyagent-runs/{tenant}/{app_id or -}/{run_id}.json in the metadata namespace, on submit and on terminalStudioDurable—
agent-service run files (run.json, events.jsonl, trace.jsonl, output.json, feedback, evidence, _idem, _specs, _quota)Pod-local under the scratch dir; mirrored to blob-store namespace agent_runs (≤ 500 events / trace rows), which is the read fallbackagent runner → /agents/runs/{id}/trace, …/eventsblob copy durable; the local index is not (the StatefulSet has no PVC)the local run index and any run in flight
agent-service slotsAGENT_RUNS_SLOTS=redis → agent-runs:{env}:t:{tenant} zset, :run:{run_id}, daily :d:{tenant}:{day} (2 d); preview does not set it, so slots and daily counts are in-process per podagent runnerper podall slot state
Usage ledgerPostgres database agent-service, table consumption_event (event_id unique, tenant_id, user_id, resource_urn, tokens_in/out, cost, flow, surface, ref, run_id, parent_run_id, studio_invocation_id, model, tool_calls, steps, duration_ms, status, limits_source, details)agent-service metering → GET /api/v1/usageDurable, no retention—

Analysis jobs (Detect / Explain)​

Redis only, no blob copy: studio:analysis:{tenant}:job:{id} (24 h), result:{id} (24 h), fp:{fingerprint} (6 h), latest:{app}:{analysis}:{scope} (24 h), lock studio:de:lock:{tenant}:{fp} (35 min), slots studio:de:slots:global:{cls} / studio:de:slots:t:{tenant}:{cls}, queue studio:de:queue:{tenant}, budget studio:de:budget:{tenant}:{day}. agent-service persists the detect result CSVs behind GET /analysis/results/{id}; Studio's job is the only index of them. Analysis jobs are not invocations today (see "changing").

Data apps, versions, permissions, snapshots​

StateWhereWriter → readersDurability / retentionRestart loses
App record with grants and version pointersapps/{appId}/app.json (metadata ns)DataAppService → UI, MCP, the frame's query routeDurable—
Versions (manifest, validated bundle path, state)apps/{appId}/versions/{n}.json; bundle zip in the data ns at the version's blob_pathupload/complete/publishDurable. A private draft reports expiresAt = created + BSA_DRAFT_TTL_DAYS (30) but nothing deletes it—
Permissionsrows on the app record (`principal type usertenant, readwritepublish
Tenant profile, model profilestenants/{tenant}.json (incl. semanticRouting), profiles/{model}.jsonsettings PUT, design toolsDurable—
App cache (declared persistence)Redis studio:{tenant}:app:{app}:cache:{key} + key set …:cache-keys; 1 000 keys / app, 64 KiB / valuethe app via /api/data-apps/{id}/cache/* and the fn/1 cache capabilityTTL = min(requested, declared); best effort—
App blobs (declared persistence)data ns {appId}/store/{name} → blob-store {tenant}/data-apps/{appId}/store/the app, design tools, the fn/1 blob capabilityDurable—
Links and schedules (Schedule Mode)links/{tenant}/{appId}/{linkId}.json, schedules/{tenant}/{appId}/{id}.json; the cron itself in scheduling-service (payload carries the HMAC cron grant)schedule routes → the worker's mint_access, deliverDurable—
Snapshot runsdata ns {appId}/snapshots/{runId}/{started.json, run.json, uploads.json, summary.json, delivery.json, page.png, page.pdf, tab-N.png}Studio writes started.json; the worker writes artifacts by signed URL and run.json; Studio writes the delivery receiptDurable, no TTL—
Send claims (Schedule Mode dedupe)`bsa-claim:sched-send:{tenant}:{app}:{sid-}:{fire_key}:{recipient_hash}, TTL BSA_SEND_CLAIM_TTL_DAYS(35 d); backendredison preview, off in prod;postgresraisesClaimStoreUnavailable` → the deliver route fails openinternal_deliver_runbest effort
Chat thread indexchat_threads/{tenant}/{appId}/{userId}/… (transcripts live only in agent-service)chat routesDurable—
Notebooksnotebooks/{id}/{notebook,versions/{n},results/{n}}.json, files at data ns notebooks/{id}/files/…notebooks serviceDurable—
Audit eventsaudit/{YYYY-MM-DD}/{HHMMSS}-{evt_id}.json (append-only; SQL: audit_events)every mutating service, best effortDurable—

Studio in-memory state (lost on a Studio pod restart)​

In-process workflow runs on the deprecated path; :submit llm/lookup/classify tasks (reaped to deadline_exceeded); viewer-mode capability channels (the long-poll a sandbox's query/agent/llm.call rides); in-process eval runs; the MCP tools/list cache (30 s); TTL caches (token validate 300 s, session 30 s, OAuth introspection ≤ 30 s, support-login sessions 240 s, agents/connections 60 s, actions catalog 300 s, pricing book, semantic catalog, query-path manifests); the snapshot-session rate limiters.

Changing (in flight on platform, check git log before relying on this page)​

  • #48, Studio's copies go. At a78212f Studio still ships workflows/{runner,validate,plan,registry,cel,llmrun, sqlrun,classify,graph,partitions,templates,lock}.py and workflows/spec/*.json, marked deprecated/frozen, and factories.py still constructs InProcessRunner when BSA_WORKFLOW_RUNNER=inprocess (the code default; preview sets remote and BSA_WORKFLOW_ENGINE=runtime). The runtime side landed in b21a421 ("runtime-server answers what Studio kept a copy of: registry build facts, gated changes, CEL, llm.call output"). When the Studio deletion lands, the in-process-only rows above (Studio-side artifact versions, pointers, fingerprints, ledger claims) become read-only history, and inprocess/shadow modes disappear; rollback is git revert of that commit (see operations.md (in the bicycle-studio-api repository, not published)).
  • #39, one run list. GET /api/studio/v1/runs federates only function and workflow today (runtime_broker/runs.py SOURCES). The frozen v1.5 design mirrors agent runs, analysis jobs and Schedule Mode runs into the invocation store with deterministic ids, adds run_status (partial), makes llm/agent draft runs ordinary inv_ records, and replaces the per-invocation index keys with one day document invocations/{tenant}/idx/{yyyymmdd}.json maintained read-modify-write. None of that is on platform at a78212f; the per-invocation key index in this page is what runs.
  • Action policy landed in a78212f (row above); the runtime's sends.auto_refusal (contract 1.6.0) is the one implementation, Studio passes facts.action_policy and facts.actions_allowed.
  • Snapshot identity (P0.2): the app-scoped render token is built but parked; the worker still renders as the tenant support session and refuses apps not shared with the whole tenant.
  • Postgres claim store: the postgres backend is a stub on both sides; the Redis bridge claim is the only send dedupe outside the runner's ledger.