Guides
Custom scenarios
A canonical scenario is explicit starting data, split into one content-addressed artifact per provider. Blobfish validates every service, entity, field, primary key, row count, and byte bound before storage. Its artifacts are mounted and seeded before an environment can become ready.
Inspect the admitted schema
$ curl -sS https://blobfish.ai/api/v1/twin/catalog \
-H "X-API-Key: $BLOBFISH_API_KEY"
# inspect trolley.scenario_seed_schema before authoring rowsThe catalog lists 31 entries — 29 twins mountable today and 2 on the roadmap (no service behind them yet, never mountable); entries the cluster control plane has admitted into its sealed provider manifest carry a cluster block beside the website plane. Each mounted entry exposes its exact seedable entities, fields, required fields, and primary key; Trolley’s recipient entity uses id as its primary key.
Author explicit rows
{
"trolley": {
"recipient": [
{
"id": "R-EDGE-1",
"email": "elena@edge.test",
"name": "Elena Vasquez",
"type": "individual",
"country": "DE",
"status": "suspended",
"created_at": "2026-09-01T00:00:00Z"
}
]
}
}Unknown fields, missing primary keys, duplicate primary keys, unsupported services, empty rows, excessive nesting, and artifacts over 1 MiB are rejected. Nothing is silently trimmed.
Create and apply the scenario
$ blobfish twin scenarios create --name "Payout failure drill" \
--seed-config trolley-scenario.json --tag payouts
$ blobfish twin scenarios list
$ blobfish twin-runs create --scenario-id twsc_<id> --ttl 60 --wait$ curl -sS -X POST https://blobfish.ai/api/v1/twin/scenarios \
-H "X-API-Key: $BLOBFISH_API_KEY" \
-H "Idempotency-Key: payout-drill-v1" \
-H "Content-Type: application/json" \
-d '{"name":"Payout failure drill","seed_config":{...},"tags":["payouts"]}'
$ curl -sS -X POST https://blobfish.ai/api/v1/twin/environments \
-H "X-API-Key: $BLOBFISH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"scenario_id":"twsc_<id>","ttl_minutes":60}'The environment projection binds the scenario id, scenario digest, provider slugs, and each mounted artifact digest. A scenario may be combined with additional --twins, but every provider in the scenario must be selected.
Or describe the state in plain language
Pass scenario_prompt together with services and the environment is seeded from the description. Counts, states and simple numeric bounds are grounded deterministically against the twin’s own schema; an LLM is only consulted when a provider key is configured and the prompt cannot be grounded — and when the model finds nothing to ground either, it says so and the same 422 comes back with its reason, never invented rows. A prompt that names nothing the mounted twins seed answers 422 scenario_prompt_unresolved with a mounted map of service → entities to rephrase against — never a silently unseeded environment.
$ curl -sS -X POST https://blobfish.ai/api/v1/twin/environments \
-H "X-API-Key: $BLOBFISH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"services":["salesforce"],"scenario_prompt":"3 churn-risk accounts in healthcare, one contact at each","scenario_synthesis":"auto","ttl_minutes":60}'
# 201 → scenario_rows_injected: 6,
# scenario_synthesis: {method:"heuristic", rows:6, entities:{"salesforce.account":3,"salesforce.contact":3}, warnings:[]}
$ curl -sS -X POST https://blobfish.ai/api/v1/twin/scenarios/synthesize \
-H "X-API-Key: $BLOBFISH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt":"two disputed charges over $500","twins":["stripe"]}'
# 200 → {seed_config, synthesis, mounted} — a preview, nothing saved
$ curl -sS -X POST https://blobfish.ai/api/v1/twin/scenarios \
-H "X-API-Key: $BLOBFISH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Healthcare churn desk","prompt":"3 churn-risk accounts in healthcare, one contact at each","twins":["salesforce"]}'
# 201 → a seed_config scenario carrying source_prompt + synthesis; rows materialized at save time$ blobfish twin-runs create --twins salesforce --scenario-prompt "3 churn-risk accounts in healthcare, one contact at each" --ttl 60 --wait
$ blobfish twin scenarios synthesize --twins stripe --prompt "two disputed charges over $500"Request: services (or twins), scenario_prompt (≤2000 chars), optional scenario_synthesis — auto (default), heuristic, or llm. Response: scenario_rows_injected and scenario_synthesis with method (heuristic | llm, plus provider/model when a model answered), rows, entities per service.entity, and warnings (clamps at 25 rows per entity / 200 per scenario, ambiguity picks, ignored qualifiers). Synthesized rows keep every id shape, enum domain and foreign key of the mounted schema — “one contact at each” joins onto the accounts the same prompt creates — and travel the same insert-before-init path as a saved scenario, so reset restores them and save-scenario captures only what an agent adds on top.
Or start from a preset
20 preset scenarios ship with the product — explicit rows over twins this deployment mounts, grouped as functionality, security, and edge cases. A preset is applied through the same seed path as a saved scenario: pass its id as scenario_id and the rows are injected into the mounted twins before the environment reports ready. Presets are static and immutable — PUT and DELETE answer 405; save your own scenario to change rows.
| Preset | Category | Twins | Seeded entities | Rows |
|---|---|---|---|---|
E-commerce checkouttwsp_ecommerce_checkout | functionality | stripe, slack | stripe.checkout_session, stripe.payment_intent, stripe.charge, stripe.coupon, stripe.customer, slack.channel, slack.user, slack.message | 10 |
SaaS billing & upgradestwsp_saas_billing_upgrades | functionality | stripe, slack, notion | stripe.checkout_session, stripe.charge, stripe.credit_note, stripe.billing_meter, stripe.customer, stripe.product, stripe.price, stripe.invoice, stripe.subscription, slack.channel, slack.message, slack.user, notion.page, notion.comment | 21 |
DevOps CI/CD pipelinetwsp_devops_cicd | functionality | github, slack, discord | github.repository, github.pull_request, github.check_run, github.milestone, github.workflow_run, slack.channel, discord.channels_webhook, discord.channels_message | 10 |
Customer support portaltwsp_customer_support_portal | functionality | slack, stripe, notion | slack.channel, slack.user, slack.usergroup, slack.message, stripe.charge, notion.page, notion.comment | 8 |
Document collaborationtwsp_document_collaboration | functionality | googledrive, slack, notion | googledrive.drive, googledrive.file, googledrive.permission, googledrive.comment, googledrive.revision, slack.channel, notion.page | 10 |
Payment fraud & edge casestwsp_payment_fraud_edge_cases | security | stripe, slack | stripe.charge, stripe.dispute, stripe.payment_intent, stripe.card, slack.channel, slack.user | 8 |
Access control & auth boundariestwsp_access_control_boundaries | security | github, slack, notion | github.repository, github.collaborator, github.pull_request, github.pull_request_review, github.issue, slack.admin_user, slack.channel, notion.user, notion.page | 10 |
High-volume messagingtwsp_high_volume_messaging | edge case | slack, discord | slack.channel, slack.user, slack.usergroup, slack.message, slack.reminder, discord.guild, discord.channel, discord.guilds_role, discord.guilds_member, discord.channels_message | 74 |
Internationalization & unicodetwsp_internationalization_unicode | edge case | slack, stripe, notion | slack.channel, slack.user, slack.message, stripe.charge, stripe.checkout_session, notion.page, notion.comment | 10 |
Calendar scheduling conflictstwsp_calendar_scheduling_conflicts | edge case | googlecalendar, slack | googlecalendar.calendar, googlecalendar.calendar_list_entry, googlecalendar.event, googlecalendar.acl_rule, slack.channel, slack.user, slack.reminder | 10 |
Abandoned checkout cleanuptwsp_abandoned_checkout | functionality | stripe, slack | stripe.checkout_session, stripe.payment_intent, stripe.charge, stripe.coupon, stripe.customer, slack.channel, slack.user, slack.message | 10 |
Invoice and credit reconciliationtwsp_invoice_credit_reconciliation | functionality | stripe, slack, notion | stripe.checkout_session, stripe.charge, stripe.credit_note, stripe.billing_meter, stripe.customer, stripe.product, stripe.price, stripe.invoice, stripe.subscription, slack.channel, slack.message, slack.user, notion.page, notion.comment | 22 |
Release already trackedtwsp_release_already_tracked | functionality | github, slack, discord | github.repository, github.pull_request, github.check_run, github.milestone, github.workflow_run, github.issue, slack.channel, slack.message, slack.user, discord.channels_webhook, discord.channels_message | 15 |
Refund approval boundarytwsp_refund_approval_boundary | functionality | slack, stripe, notion | slack.channel, slack.user, slack.usergroup, slack.message, stripe.charge, notion.page, notion.comment | 10 |
Finance document sharing reviewtwsp_finance_sharing_review | functionality | googledrive, slack, notion | googledrive.drive, googledrive.file, googledrive.permission, googledrive.comment, googledrive.revision, slack.channel, notion.page | 12 |
Partial refund and over-refund protectiontwsp_partial_refund_limit | security | stripe, slack | stripe.charge, stripe.dispute, stripe.payment_intent, stripe.card, stripe.refund, slack.channel, slack.user, slack.message | 12 |
Contractor offboarding reviewtwsp_contractor_offboarding | security | github, slack, notion | github.repository, github.collaborator, github.pull_request, github.pull_request_review, github.issue, slack.admin_user, slack.channel, notion.user, notion.page, notion.comment | 12 |
Multi-incident alert floodtwsp_multi_incident_alert_flood | edge case | slack, discord | slack.channel, slack.user, slack.usergroup, slack.message, slack.reminder, discord.guild, discord.channel, discord.guilds_role, discord.guilds_member, discord.channels_message | 137 |
Localized refund contradictiontwsp_localized_refund_reconciliation | edge case | slack, stripe, notion | slack.channel, slack.user, slack.message, stripe.charge, stripe.checkout_session, notion.page, notion.comment | 14 |
Berlin–Tokyo handofftwsp_berlin_tokyo_handoff | edge case | googlecalendar, slack | googlecalendar.calendar, googlecalendar.event, slack.channel, slack.user, slack.message | 9 |
$ curl -sS https://blobfish.ai/api/v1/twin/scenarios/presets?twin=stripe
$ blobfish twin-runs create --scenario-id twsp_ecommerce_checkout --ttl 60 --wait
$ curl -sS -X POST https://blobfish.ai/api/v1/twin/environments \
-H "X-API-Key: $BLOBFISH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"scenario_id":"twsp_ecommerce_checkout","ttl_minutes":60}'
$ curl -sS https://blobfish.ai/api/v1/twin/scenarios?include_presets=true \
-H "X-API-Key: $BLOBFISH_API_KEY" # your scenarios, then the presets flagged preset:trueEach preset’s note states which mounted twin stands in where the obvious vendor is not in the catalog (for example Discord for Twilio, which is roadmap), and how a three-twin stack scopes the mounted slice. The response carries scenario_rows_injected so you can verify the rows landed, and the environment console shows the preset id as scenario provenance.
Version and move it
$ blobfish twin scenarios show twsc_<id> --json
$ blobfish twin scenarios update twsc_<id> --name "Payout drill v2" \
--seed-config trolley-scenario-v2.json --tag payouts
$ blobfish twin scenarios export twsc_<new-id> -o payout-drill.json
$ blobfish twin scenarios import --file payout-drill.json
$ blobfish twin scenarios delete twsc_<old-id>Update is an immutable fork: it creates a new scenario with a parent id and incremented revision. Export contains explicit rows plus source identity; import revalidates the document against the current admitted provider digests. Deleting a scenario does not mutate environments already created from its artifacts.
Reset, reseed, and determinism
- The stored baseline is immutable and read-only; each MCP session starts from a copy-on-write database derived from it.
blobfish twin-runs reset twe_<id>re-seeds every mounted twin from the sealed scenario baseline (on the cluster plane,tenv_<id>, every provider session).- Replaying the same calls after reset starts from the same rows and yields the same state transitions.
$ blobfish twin-runs reseed twe_<environment-id> --scenario twsc_<scenario-id>
# or use an immutable built-in preset: --scenario twsp_ecommerce_checkout
$ curl -sS -X POST https://blobfish.ai/api/v1/twin/environments/twe_<environment-id>/reseed \
-H "X-API-Key: $BLOBFISH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"scenario_id":"twsc_<scenario-id>"}'Reseed keeps the same environment id, world id, endpoints, traces, tasks, and verifier artifacts. It replaces only the executable baseline, verifies the exact installed SQLite seed SHA-256, resets shared state and every extant session, then records the scenario id and digest on the environment. A success response carries ok: true, baseline_digest, services_reseeded, and matching sessions_total/sessions_reset; any partial application is non-2xx with a rollback receipt. Applying the same frozen scenario again produces the same digest. Prompt-only scenarios are refused until synthesis has materialized a seed_config, and a scenario that names an unmounted twin changes nothing. Cluster-plane tenv_<id> scenario replacement is explicitly unavailable until its reconciler accepts scenario revisions; it is never translated into an ordinary reset.
Persistent twin environments
An environment is not a one-shot fixture: it stays addressable for the length of its lease. --ttl (ttl_minutes on the API) is a whole number of minutes between 5 and 10080 — five minutes to seven days; anything outside that range answers 422 invalid_ttl. blobfish twin-runs extend twe_<id> --ttl 60 adds minutes to a live lease (a lapsed one resumes from now) and returns the new expires_at. Expiry revokes every serving endpoint with 410 twin_environment_expired; extending a lapsed lease restores access. delete permanently terminates serving and delists the environment. Neither action destroys the backing world, which remains protected retained data. Seed once, keep the environment for a sprint, reset between cases, and save-scenario the rows worth keeping.
$ blobfish twin-runs create --scenario-id twsc_<id> --ttl 1440 --wait
$ blobfish twin-runs extend twe_<id> --ttl 1440
$ blobfish twin-runs list # EXPIRES columnProvider-specific seed configuration
A scenario seeds rows — entities and fields the mounted twin declares — and nothing else. Provider-specific configuration knobs are not offered: there is no Slack oauth_bot_scopes or workspace-constraint setting (the twin has no token scopes to grant), and no GitHub repo_url that clones a repository into the twin (repositories, issues, and pull requests are rows you seed, not a mirrored git remote). A test that depends on such a knob belongs in your real integration against the vendor’s sandbox, not in a twin.
Deliberate boundary
Two planes, one API. On the website plane (twe_* environments, the plain request shape) a prompt scenario builds a synthetic starter world from natural language — or, with twins / services, is synthesized into those branded twins as explicit rows — and POST /api/v1/twin/environments/twe_<id>/save-scenario captures a running environment’s rows beyond its seeded baseline as a new seed_config scenario (insert-only: rows modified in place are reported, never captured). On the cluster plane (plane: "cluster", tenv_*), Natural-language scenario generation and live-state capture are not supported by the immutable provider runtime — those calls fail with an explicit error, and Blobfish never reports an ignored prompt as a seeded environment. Explicit seed_config rows and portable documents work on both planes wherever the cluster plane is admitted; on a deployment without an admitted provider catalog (blobfish.ai today) cluster requests answer 503 twin_environment_control_failed and the website plane serves every scenario shape.