Skip to content

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

Provider catalog
$ curl -sS https://blobfish.ai/api/v1/twin/catalog \
    -H "X-API-Key: $BLOBFISH_API_KEY"
# inspect trolley.scenario_seed_schema before authoring rows

The 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-scenario.json
{
  "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

CLI
$ 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
HTTP
$ 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.

HTTP
$ 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
CLI
$ 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.

PresetCategoryTwinsSeeded entitiesRows
E-commerce checkout
twsp_ecommerce_checkout
functionalitystripe, slackstripe.checkout_session, stripe.payment_intent, stripe.charge, stripe.coupon, stripe.customer, slack.channel, slack.user, slack.message10
SaaS billing & upgrades
twsp_saas_billing_upgrades
functionalitystripe, slack, notionstripe.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.comment21
DevOps CI/CD pipeline
twsp_devops_cicd
functionalitygithub, slack, discordgithub.repository, github.pull_request, github.check_run, github.milestone, github.workflow_run, slack.channel, discord.channels_webhook, discord.channels_message10
Customer support portal
twsp_customer_support_portal
functionalityslack, stripe, notionslack.channel, slack.user, slack.usergroup, slack.message, stripe.charge, notion.page, notion.comment8
Document collaboration
twsp_document_collaboration
functionalitygoogledrive, slack, notiongoogledrive.drive, googledrive.file, googledrive.permission, googledrive.comment, googledrive.revision, slack.channel, notion.page10
Payment fraud & edge cases
twsp_payment_fraud_edge_cases
securitystripe, slackstripe.charge, stripe.dispute, stripe.payment_intent, stripe.card, slack.channel, slack.user8
Access control & auth boundaries
twsp_access_control_boundaries
securitygithub, slack, notiongithub.repository, github.collaborator, github.pull_request, github.pull_request_review, github.issue, slack.admin_user, slack.channel, notion.user, notion.page10
High-volume messaging
twsp_high_volume_messaging
edge caseslack, discordslack.channel, slack.user, slack.usergroup, slack.message, slack.reminder, discord.guild, discord.channel, discord.guilds_role, discord.guilds_member, discord.channels_message74
Internationalization & unicode
twsp_internationalization_unicode
edge caseslack, stripe, notionslack.channel, slack.user, slack.message, stripe.charge, stripe.checkout_session, notion.page, notion.comment10
Calendar scheduling conflicts
twsp_calendar_scheduling_conflicts
edge casegooglecalendar, slackgooglecalendar.calendar, googlecalendar.calendar_list_entry, googlecalendar.event, googlecalendar.acl_rule, slack.channel, slack.user, slack.reminder10
Abandoned checkout cleanup
twsp_abandoned_checkout
functionalitystripe, slackstripe.checkout_session, stripe.payment_intent, stripe.charge, stripe.coupon, stripe.customer, slack.channel, slack.user, slack.message10
Invoice and credit reconciliation
twsp_invoice_credit_reconciliation
functionalitystripe, slack, notionstripe.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.comment22
Release already tracked
twsp_release_already_tracked
functionalitygithub, slack, discordgithub.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_message15
Refund approval boundary
twsp_refund_approval_boundary
functionalityslack, stripe, notionslack.channel, slack.user, slack.usergroup, slack.message, stripe.charge, notion.page, notion.comment10
Finance document sharing review
twsp_finance_sharing_review
functionalitygoogledrive, slack, notiongoogledrive.drive, googledrive.file, googledrive.permission, googledrive.comment, googledrive.revision, slack.channel, notion.page12
Partial refund and over-refund protection
twsp_partial_refund_limit
securitystripe, slackstripe.charge, stripe.dispute, stripe.payment_intent, stripe.card, stripe.refund, slack.channel, slack.user, slack.message12
Contractor offboarding review
twsp_contractor_offboarding
securitygithub, slack, notiongithub.repository, github.collaborator, github.pull_request, github.pull_request_review, github.issue, slack.admin_user, slack.channel, notion.user, notion.page, notion.comment12
Multi-incident alert flood
twsp_multi_incident_alert_flood
edge caseslack, discordslack.channel, slack.user, slack.usergroup, slack.message, slack.reminder, discord.guild, discord.channel, discord.guilds_role, discord.guilds_member, discord.channels_message137
Localized refund contradiction
twsp_localized_refund_reconciliation
edge caseslack, stripe, notionslack.channel, slack.user, slack.message, stripe.charge, stripe.checkout_session, notion.page, notion.comment14
Berlin–Tokyo handoff
twsp_berlin_tokyo_handoff
edge casegooglecalendar, slackgooglecalendar.calendar, googlecalendar.event, slack.channel, slack.user, slack.message9
Apply a preset
$ 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:true

Each 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

Versioned workflow
$ 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

  1. The stored baseline is immutable and read-only; each MCP session starts from a copy-on-write database derived from it.
  2. 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).
  3. Replaying the same calls after reset starts from the same rows and yields the same state transitions.
Replace an existing baseline
$ 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.

Terminal
$ blobfish twin-runs create --scenario-id twsc_<id> --ttl 1440 --wait
$ blobfish twin-runs extend twe_<id> --ttl 1440
$ blobfish twin-runs list        # EXPIRES column

Provider-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.

Next

Gateway & health · Twin reference · Quickstart