Scripts, agents and notifications
#Developer access
Scripts and agents act on an account through access tokens (Account, then Developer): the REST API under /api/v1 and the Studio MCP server at /mcp/studio share one validation path with the run builder, so a scenario an agent authors over MCP is held to the same rules as one saved by hand. Tokens are shown once, stored hashed, scoped, and optionally expiring, up to the plan's count; minting one needs a signed-in browser session, so a token never mints a token. An MCP client that supports sign-in needs no token at all: add /mcp/studio to it, approve it on a Daishi page that shows what it asks for, and it is listed under Account, then Developer, as a connected app. A connected app counts toward the same allowance as a token, works on the MCP server only, never sees your email address, and stops working the moment you disconnect it. Sign out everywhere and a password reset revoke every token and disconnect every app at once. The developer docs are the reference, and the Plans table says which plans may write through it. Scripts can skip raw HTTP with the client libraries: pip install daishi for Python and npm install daishi-sdk for TypeScript and JavaScript, one typed method per endpoint, with retries and helpers that wait for a run to finish. To have an AI assistant design, run and read a study for you, install the study design skill.
Onboard your agent, at the top of the Studio's Overview once you are signed in, connects the AI coding tool or chat app you already use without a token. Its marks show the coding tools the setup lines cover: Claude Code, Codex, Cursor, Gemini CLI, VS Code and Windsurf. One click copies a setup prompt. Pasted into a coding tool, it adds the Studio MCP server, starts the sign-in for you to approve, and checks the connection with whoami before it does anything else. A chat app cannot change its own settings, so it lists where to add the server as a custom connector, then makes the same check when you come back and say whoami. The prompt carries no key or token, and it asks your agent to estimate before any launch and to launch, cancel, delete or publish only when you say so. The X beside the button, which appears when the pointer is over it and always shows on a touch screen, puts it away in that browser; the same button stays under Account, then Developer, with the full prompt to read before you paste it.
#Reports for scripts and agents
Everything the Studio shows about a batch or an experiment is also a JSON report a script or an agent can read, and the per-turn record each seat wrote can be paged through the same way:
- Batch reports.
GET /api/v1/batches/<id>over the developer API, or thebatch_reporttool on the Studio MCP, returns a series as one report: per model the 95% interval with the mean, the interquartile mean and survival as a rate, every trial's per-seat outcome and seeds, the cost so far and the verdict sentence, exactly as the batch page prints them. - Experiment reports.
GET /api/v1/experimentslists an account's experiments with their status and verdict,GET /api/v1/experiments/<id>returns the full report (arms, pairs, comparisons, the rule and the decision) with every trial's record grouped by arm, the card's plain sentence (plain) and what each arm changed from the control (what_differed), andPOST /api/v1/experiments/<id>/cancelstops one, with your reason and, if you have one, the experiment that replaces it;POST /api/v1/experiments/<id>/stop-reasongives the reason once to an experiment that ended without one.POST /api/v1/experiments/<id>/noteadds an entry to your note,POSTorGET /api/v1/experiments/<id>/designattaches or reads the design document, the read with the outside witnesses' proofs of its digest and every earlier version, andGET /api/v1/experiments/<id>/creationreads the creation record with the proofs of its digest. The MCP tools arelist_experiments,get_experiment,estimate_experiment,create_experiment,cancel_experiment,set_experiment_stop_reason,add_experiment_note,attach_experiment_design,get_experiment_designandget_experiment_creation. - Runs, seats and models.
GET /api/v1/runspages through the whole run history, newest first: sendnext_cursorback ascursorfor the next page.GET /api/v1/runs/<id>/seats(run_seatson the MCP) returns what a run's Seats section shows, seat by seat, with the trajectory file to read, andGET /api/v1/models(get_models) returns the figures the Models page draws. - Trajectories.
GET /api/v1/runs/<id>/logslists the files kept for a run (per seat, a trajectory with one line per model call, the observation sent, the reply and the action that ran, and a usage record), andGET /api/v1/runs/<id>/logs/<file>returns one. On the MCP,list_trajectorieslists them andread_trajectorypages through one by line offset, up to a thousand lines a call, so an agent can audit a finished run without pulling the whole file. - Publishing.
POST /api/v1/runs/<id>/publish, with an optionalnote, does what the share block's Publish to profile does, andPOST /api/v1/runs/<id>/unpublishundoes it; the MCP tools arepublish_runandunpublish_run.POST /api/v1/experiments/<id>/publishandPOST /api/v1/batches/<id>/publishdo what Publish study does, every trial that played in one call, and answerstudy_playingwhile a trial is still queued or playing; each has anunpublishtwin, and on the MCP they arepublish_experiment,unpublish_experiment,publish_batchandunpublish_batch. All of them, and writing the declared result, needruns:publish, which no token or app gets unless the owner ticks it when minting the token or approving the app.runs:writenever includes it, so an agent that may launch cannot speak for the owner in public unless they chose that too. - Run sets.
POST /api/v1/setscreates a set (its name, the setups runs are filed under, a note and whether the card prints each seat's own text),POST /api/v1/sets/<id>/membersand.../members/removefile finished runs and whole series under a setup or take them out,POST /api/v1/sets/<id>/setups/<setup>/designattaches a setup's design document,DELETE /api/v1/sets/<id>deletes the set (its runs stay), andGET /api/v1/setsandGET /api/v1/sets/<id>read them, all withruns:writeorruns:read. Changing the note, the labels or the seat text switch (POST /api/v1/sets/<id>) and publishing or unpublishing (.../publish,.../unpublish) needruns:publish, and so does any change to the runs or designs of a published set, or deleting one, since it changes what the profile shows. The MCP tools arecreate_run_set,list_run_sets,get_run_set,update_run_set,add_run_set_members,remove_run_set_members,attach_run_set_design,get_run_set_design,publish_run_set,unpublish_run_setanddelete_run_set.
All of it is scoped to the account that owns the runs and reads with a token carrying runs:read; creating or cancelling an experiment, giving its stop reason or attaching its design, and building a run set need runs:write and a paid plan, and publishing and adding to an experiment's note need runs:publish and a paid plan. The shapes, the error codes and the client setup are in the developer docs.
#When a run ends
A long series is something to be told about, not to poll, and an agent operating the lab on its own needs a callback rather than a browser tab. Under Account, then When a run ends, an account can set two things:
- A webhook address (up to 512 characters). The platform POSTs a signed JSON notice there when a unit of work ends. Only
https://addresses on public hosts are accepted: no username or password in the address, and no localhost, private, loopback or link-local host. The host is checked on the addresses its name resolves to, not only on how it is written: a name that resolves to any such address is refused when you save it, and it is resolved and checked again before every delivery, which connects only to the addresses that check approved. Leave it blank to send nothing. - Email: never, when a series or experiment ends (the default), or after every run. Mail goes only to a verified address.
Send a test (one every ten seconds) posts a test event to the address at once, with the same envelope and signature as a real notice, and shows the answer, so a receiver can be checked before anything finishes. The card also shows the last delivery: when, which event, and the status or the error.
#The events
One notice per unit of work:
run.endedfor a run launched on its own (a trial inside a series is not announced by itself; its series is).batch.endedonce, when the last trial of a series turns terminal.experiment.endedonce, when a paired experiment's stopping rule or its last trial ends it. A run inside an experiment is announced through the experiment, never on its own. An experiment created with acallback_urlsends this notice to that address instead of the account's; the email follows the account's choice either way.test, only from Send a test: its block istest, with amessagein place of a run, batch or experiment.
The body is a JSON object with type, delivery_id (unique per notice), at_ms (milliseconds since the epoch), and one block named after the kind of work:
run:id,name,status(finished,failedorcancelled),stop_reason,batch_id,experiment_id,url.batch:id,name,trials,finished,failed,cancelled,url.experiment:id,name,status,stop_reason,url.
Batches and experiments also carry verdict: the report's own reading in a few plain sentences, the limitation first, exactly as the report prints it. url is the absolute link to the thing in the Studio. Nothing from inside a match travels in a notice: no seat text, no messages, no scores per turn. A series that ended, for example:
{
"type": "batch.ended",
"delivery_id": "dlv_5f0c2a9e7b1d4c38a2",
"at_ms": 1790526270562,
"batch": { "id": "bat_ec3a38c28afdfb5c42", "name": "Famine, 10 trials", "trials": 10,
"finished": 10, "failed": 0, "cancelled": 0,
"url": "https://daishi.ai/studio#batch=bat_ec3a38c28afdfb5c42" },
"verdict": "The widest interval here is plus or minus 5.9 index points, about a grade band, so only large gaps between models mean anything yet."
}#Verifying a notice
Each POST is content-type: application/json and carries three headers: X-Daishi-Event (the event type), X-Daishi-Delivery (the delivery_id) and X-Daishi-Signature, which is sha256= followed by the hex HMAC-SHA256 of the exact request body under your account's signing secret. The key is the secret exactly as the card shows it, as text (its UTF-8 bytes), not decoded from hex. The secret is shown on the card (Reveal) and is derived for your account rather than stored, so Rotate replaces it at once, with no overlap: the new secret exists only from that moment, and a notice sent before your receiver has it fails the check. A refusal is a 4xx, which ends that notice's retries, so rotate when nothing is about to end, paste the new secret into your receiver straight away, and press Send a test to confirm the receiver takes it. A notice lost this way loses nothing else: the run, series or experiment it would have announced is in the Studio and the API as always.
Verify it the same way in any language:
raw = the request body, as bytes, before any parsing
expected = "sha256=" + hex(hmac_sha256(key = secret, message = raw))
if not constant_time_equal(expected, header["X-Daishi-Signature"]): refuse
notice = parse_json(raw)
if now_ms() - notice.at_ms > 5 minutes: refuse
if notice.delivery_id was accepted before: answer 2xx and do nothing
remember notice.delivery_id for at least 5 minutesCompare the two strings in constant time, and compute the HMAC over the raw bytes, not over a re-serialized copy of the parsed JSON. The signature proves who wrote a body, not when: anyone who has seen a notice, in a log or at a relay, can post the same bytes again and the signature still checks. at_ms and delivery_id are inside the signed body, so they cannot be changed without breaking it, and the checks after the signature stop such a replay: a copy older than a few minutes is refused, and a delivery_id you have already taken is not acted on twice. Every retry of a notice carries the same at_ms and delivery_id, and the last retry leaves about three minutes after at_ms, so a five-minute window takes every genuine attempt as long as your clock is right. Answer with any 2xx status once you have taken the notice; a body is not needed.
#Delivery and retries
A delivery times out after ten seconds. On a network error, a timeout or a 5xx answer the platform tries again after 5 seconds, then 25, then 125, four attempts in all; a 4xx answer is taken as your receiver's decision and stops the retries. A host name that does not resolve counts as a network error. One that now resolves to a loopback, private or link-local address gets no connection at all: the delivery is recorded as refused and not retried. Notices are sent once each and may arrive out of order; delivery_id tells a repeated attempt from a new notice. A notice that never gets through is logged on the platform and shown as the last delivery on the card; it never affects the run it reports on.