Skip to main content

Interactions

Synced from bicycle-studio-api

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

One sequence diagram and a short narrative per flow, as the code on platform runs it (Studio API a78212f, data-app-runtime b21a421, studio-snapshot-worker 2241304, agent-service 95c607eb4). Route shapes are in API.md (in the bicycle-studio-api repository, not published); identities and grants in security.md. Participant names are the systems in overview.md.

a. Function Try (POST /api/studio/v1/functions/{name}/versions/{n}:run)​

DraftRunner (runtime_broker/draft.py) dispatches by kind. Code goes through the full broker path: concurrency leases and compute-unit counters in Redis, an rt-invoke/1 grant pinned to the deployment, placement on runtime-server's exec role, one fresh gVisor pod per attempt (there is no warm pool; a cold spot node can add minutes and answers pool_cold). The pod's capability calls arrive at runtime-server's fn/1 gateway; cache and blob are served there, everything else is forwarded to Studio (runtime-caps with X-Invoke-Grant in service mode, or the viewer channel the Studio pod long-polls). llm and classify functions run in runtime-server's process (/v1/functions:run), calling agent-service or Jev; Studio records the result inline. An agent Try is an agent run (flow h) started with the inline spec. Every result is an invocation record with events; draft llm/agent runs still use the legacy inv_draft_ / inv_agent_ ids (changing under #39).

b. A function called by ref from an app (POST /api/data-apps/{app}/fn/{local}[:submit])​

The app never holds a credential and never names a ref: it calls a local name declared in manifest.functions (pinned fn:{tenant}/{name}@{n} or wf:{tenant}/{slug}@{n}, @latest refused, widening imports reviewed by a person at app publish). The reuse key is sha256 of (tenant, app, local name, ref@version, viewer subject, canonical input); it is never shared across viewers and lives in Redis plus a durable doc. Agent invocations do not take broker leases; they take the agent-runs tenant slot (max_concurrent_runs, default 4) and queue FIFO per tenant when it is full. Dispatch is pulled by the caller's own reads because the run needs the viewer's credential, which Studio never stores; a caller that stops reading keeps its place until the 600 s TTL. A wf: import ignores reuse (the workflow joins same-date runs itself). The bridge and bda.fn.call/watch/cancel live in the EBG copy app and the workflow guide; the public template does not ship them yet.

c. A function or workflow called by an agent as a tool​

Broker.agent_tool answers {ref, description, input_schema} for each granted fn: ref (expose.agents) and for wf: refs a workflow exposes to agents; agent-service presents them as fn_<name> / wf_<slug> tools. Each call comes back to Studio as a normal by-ref invocation carrying the agent run id, so the child is its own invocation with links.agent_run_id and the parent's id. A nested agent run is counted against its parent's slot when the parent is a live run of the same tenant and person held by the same agent-service process; anything else takes a slot as usual. The nested claim is caller-asserted today (security item 13). A child invocation started from an agent shows only its own events, with a link to the parent run (32294fb).

d. Called over MCP (/mcp/functions, SSE progress, cancel)​

The MCP server is the official SDK's streamable HTTP, stateless; /mcp and every toolset answer JSON except /mcp/functions, which answers SSE so a progressToken gets one notifications/progress per invocation event. Without a token the call waits up to BSA_FUNCTIONS_MCP_MAX_WAIT_SECONDS (55) and then returns a pointer to poll. Cancel arrives on a new connection; the request id is resolved through the Redis mcp-call record. OAuth clients cannot see these tools yet because the advertised scopes stop at apps:* (security item 12).

e. A workflow run​

Three triggers, one start path (WorkflowService._start → RemoteRunner.start): a person's "Run now" (surface ui, identity viewer); a cron fire (scheduling-service → the snapshot worker's data-app-workflow-tick → the tick route, once per (trigger, local minute), identity the tenant support session); an app or agent calling the workflow by ref (surface app|agent, the caller as viewer; the runner's trigger vocabulary is closed, so a by-ref call reaches it as manual while who and from where stay on Studio's run record and invocation, 031c6aa). Studio plans through runtime-server (BSA_WORKFLOW_ENGINE=runtime), passing the tenant facts so mail and action rules fail at save or plan, then pushes the revision and starts the run on the runner with the wf-run/1 grant. The runner is the only executor (BSA_WORKFLOW_RUNNER=remote). Nodes that need Studio-held credentials call Studio internal routes with the grant only; Studio answers as the tenant's support session after checking the grant's jti against the run. The runner's outbox is relayed into the run's invocations (root plus a child per node partition) so GET /invocations/{id}/events and GET /runs?kind=workflow see one trace. Try runs (env: try:<user>, any non-published revision) write versions but move no pointer and dry-run every send.

f. Snapshot node → studio-snapshot-worker​

The runner never holds bytes: Studio runs the same one-shot pipeline as a schedule's "Send test" (capture_png, no delivery), as the tenant service identity, and hands back metadata; the PNG becomes a report artifact whose blob the send step later attaches and inlines. Only tabs[0] is captured, PNG only. A capture of an app not shared with the whole tenant fails before it starts (snapshot_app_not_shared), and a page that never reached its panels (ready.ok=false with no partial panels) fails the step (snapshot_render_failed); a send that reads a snapshot that did not succeed this run is skipped rather than sent with stale data. Errors 503 (snapshot_timeout, snapshot_unavailable) are retried by the runner (2); 422 codes are final.

g. Send step: approval, mail or action, ledger, failure routing​

Dedupe is the runner's per-action ledger keyed by the rendered idempotency_key (one open attempt per key; a re-run joins or supersedes); Studio never writes it. Approval is optional since contract 1.5.0: auto sends as the step runs (AI-written input under it is a warning and a gated change a person acknowledges at publish); manual needs a person whose decision is bound to the exact payload hash; notify.on_success: send approves automatically only on a full success. An action send under auto is refused at save, at plan and again at :send unless the tenant admin marked the action auto_allowed and the deployment allows it (BSA_WORKFLOW_ACTIONS_ALLOWED, empty on preview and by default). Email goes out as the tenant's service identity (#36) through alert-service, which returns no Message-ID, so a timeout is unknown and a person decides. Failure routing mails one recipient per failed or partial prod run; deterministic classes use the routes table, everything else is Jev's choice among role names, never addresses.

h. Agent run (POST /api/data-apps/{app}/agent-runs, POST /api/studio/v1/agents/{id}/runs)​

Studio holds the tenant's limits document (staff-written) and signs the effective limits into every run; author numbers can only lower them; agent-service is the enforcer where the money is spent. Slots exist on both sides: Studio's per-tenant slot in Redis, agent-service's tenant and global in-flight pools (per pod on preview, since AGENT_RUNS_SLOTS is unset there). Writes are never agent tools: an agent reads through tool-service and hands off to a workflow send step. agent-service does not verify the caller assertion today (security item 1).

i. Data-app chat with functions​

The transcript lives only in agent-service; Studio keeps a small per-user thread index and the model card it prefetched. Functions are inherited by default (settings.chat.functions: inherit, owner/admin may set off; the API exists, the settings UI control does not). Tool calls from chat are ordinary invocations with surface: chat.

j. Detect / Explain (ad hoc)​

Analysis is a separate job model: Redis-only records, its own slots and budget, not an invocation and not in /runs (#39 v1.5 mirrors it). The workflow detect / explain kinds are status: draft and cut from v1; the runner's Executors.runnable() would attempt them if a document had them, while the registry advertises them not_runnable.

k. Data-app Schedule Mode (snapshot worker)​

Schedule Mode is the older, live path (#4): the worker's own Temporal workflow does capture, summary and delivery, all through Studio's internal routes with a support session traded from the HMAC grant (Run now and Send test use a one-shot grant and write started.json first). It is not a workflow run today; the plan is to turn each schedule into a generated workflow (snapshot node → send node) and keep the worker as the render backend only.

l. Publish with human review, disable / enable​

Gated changes (a new send, a changed to or approval, a widened import, ai_text_unreviewed under auto) need a person: the review hash must have been shown to that person for that revision within 900 s; MCP workflow_publish and function_publish refuse gated changes and hand back the review link. Functions follow the same review and TTL (BSA_FUNCTIONS_REVIEW_TTL_SECONDS). Disable is the soft delete (#44): pinned callers keep resolving the ref (function.disabled: true on their roots), starts are 409, and the confirm dialog lists dependents from GET …/dependents. Approvals the runner holds are not closed from Studio (unchecked: ["remote_approvals"]); they expire on their own.