# Simulate a campaign (/docs/build/simulate)



A simulation lets you try a campaign before it touches a real project. You paste the campaign's quests and point ledgers into Playground, and Domino builds it and publishes it to a throwaway project of its own.

* **Your projects stay as they are.** The simulation is a separate project in Domino's sandbox, in its **Test** environment. Nothing reaches your projects until you choose **Make this real**.
* **It's yours alone.** Only you, signed in to the same organization, can open your simulations. Teammates don't see them.
* **It's deleted after 24 hours.** Each simulation is deleted 24 hours after it starts, and you can't open it after that.

```mermaid
flowchart LR
    Paste["Paste the campaign"] --> Build["Build"]
    Build -->|builds| Simulation["Published to the simulation"]
    Build -->|fails| Error["Error shown"]
    Simulation --> Run["Run personas"]
    Run --> Results["Who got paid, rejects, cap hits, leaderboard"]
    Simulation --> Real["Make this real"]
    Real --> Test["Your project, in Test"]
```

## Simulate [#simulate]

1. Open **Developers → Build** with the **Test** environment selected, then choose **Simulate** beside **Map**. You need publish permission in **Test**, as for the rest of [Playground](/docs/build/playground).
2. Edit the campaign in the editor. It starts with a small example: a point ledger and three quests.
3. Choose **Simulate**, or press Ctrl+S (⌘S on a Mac) in the editor.

The build can take a few minutes. Its state shows below the editor: **Queued**, **Building**, then **Published** once the campaign is live in the simulation, or **Failed**. The simulation's deletion time is shown beside it. Simulate again after an edit and Playground reuses the same simulation while it has more than 30 minutes left and you haven't [run personas](#run-personas) in it; otherwise it starts a new one.

> [!NOTE]
> You can start 20 simulations and 60 builds an hour. The editor and your latest build are remembered in this browser, so they're still there when you come back.

## When the campaign doesn't build [#when-the-campaign-doesnt-build]

A failed build shows **The campaign didn't build** with the error from the step that failed, such as a type error with its line number. **Build logs** opens the full log. Fix the campaign in the editor and choose **Simulate** again.

> [!DEV]
> The campaign is one TypeScript module in the campaign starter's authoring style. Each top-level export made with `defineQuest`, `defineQuestType`, `defineLedger`, `defineTracker`, `defineLeaderboard`, `defineReferral`, `defineBadge`, `defineOffer`, `defineEngagementReward` or `defineDashboard` from `@domino-sdk/relay/authoring` becomes part of the campaign; other exports are ignored. The module may import only packages the campaign starter installs: a relative path, an absolute path, a `node:` module or any other package fails the build before anything compiles. A module that exports no definition fails with what to export. The module can be up to 256 KB.

## Run personas [#run-personas]

Once the simulation shows **Published**, **Run personas** sends generated members through the campaign, day by day, the way real members would use it. Their actions go through Domino's real pipeline: the same checks, caps, budgets and rewards as in your project, with points paid into the simulation's own ledgers.

1. Set **Days**, from 1 to 30. It starts at 7.
2. Set **Members per persona**. It starts at 5. A run has at most 500 members in total.
3. Choose **Run**. A bar shows the simulated days done. A run takes longer the more members and days it has.

| Persona                           | What its members do                                                                                                               |
| --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| **Power user**                    | Act on every quest every day, three times a day on quests that can be completed more than once, well past every filter.           |
| **Casual member**                 | Act on each quest once, every other day, with just enough to pass its filters.                                                    |
| **Bot-like spammer**              | Flood repeatable quests with the same minimal post or answer, past any daily cap, and repeat one-time quests.                     |
| **Just under / just over a cap**  | One persona on each side of a member's daily or lifetime cap: one stays under it, one tries once more than it allows.             |
| **Just below / at a filter**      | One persona on each side of a number a tracker quest's filter compares, such as at least 10 likes.                                |
| **One short of / reaches a goal** | One persona on each side of a progress quest's goal for a day or the whole run.                                                   |
| **Real X authors**                | The real people behind a sample of posts from each X search tracker, posting what they posted. See [Real X posts](#real-x-posts). |

The campaign gets a threshold pair for each cap, filter number and goal it has, up to 10 pairs.

Each member is enrolled in every quest and campaign that requires enrollment as soon as it's created. If that's refused, for example because the campaign has ended, the run stops and shows why. How members act on each kind of quest:

* **Quests members submit** with no answer, a text or a link: they submit, and the quest's own check decides.
* **Quests a teammate confirms** are passed as a reviewer would pass them. Caps, budgets and rewards still apply.
* **X and Discord tracker quests** get a post, message or reaction from each member. Members get their accounts before their first record, so nothing counts as having happened before they linked.
* **Custom tracker quests** whose records name members get a record from each member.
* **Quests that complete by themselves** once others are done follow from the other quests.

Photo and quiz quests, Telegram, YouTube, TikTok and wallet trackers, and quests that pay an account other than the author are listed under **Not simulated**.

At the end of each simulated day, every member's clock moves one day forward, so daily caps and goals start again.

### Real X posts [#real-x-posts]

A campaign with an X search tracker is also tried on real posts. Before its members are created, the run reads one sample of each X search tracker's search: up to 100 matching posts from the last 7 days.

* **Each author becomes a member.** When a quest pays the authors of a tracker's posts, every author in its sample becomes a **Real X authors** member, named by their handle and linked to their X account before any of their posts is sent, so none counts as having happened before they linked.
* **Their posts are replayed in order.** Each post is sent to the tracker as that author's post, with its text and numbers, oldest first. The sample's week is spread over the run's days, and an author sends at most 12 posts a day to one tracker.
* **The sample is read once.** Later runs in the same simulation replay the same posts and read nothing more from X. Samples don't count toward your organization's X usage.

Authors past the run's 500 members are left out, and Playground says how many. If a sample can't be read, for example because X search is busy, **Not simulated** says so and the run goes on without it; the next run tries again.

The results add a **Real X posts** section: each sample and its query, how many authors got paid, the replayed posts a quest's filter passed over, or that Domino or the quest rejected or refused, and why, and **Estimated X reads a day**. That estimate is what the trackers would read from X on each plan, against the plan's monthly X budget: how often each tracker searches on that plan (the interval your plan allows, as a deploy warns when a tracker asks for less), the pages a search needs for the posts its sample found, and the re-reads of [posts it already knows](/docs/build/trackers#how-often-domino-reads-x).

### Results [#results]

When the run finishes, Playground shows:

* **By persona**: points paid, attempts accepted, attempts rejected or refused, and cap hits, with a total row. A campaign with tracker quests also shows records skipped and records a quest's filter passed over.
* **Rejects and their reasons**: each reason Domino or the quest gave, the quest, and how many times. Cap and budget hits read **Budget exhausted**.
* **Leaderboard**: the top 10 members of each point ledger after any simulated day you choose.
* **Real X posts**: what the [sampled X authors](#real-x-posts) earned and the X reads a day the campaign's trackers would make, when it has an X search tracker.
* **Not simulated**: the quests the run left out, and why.

Each day, members act in rounds, and each round waits for its attempts to be checked before the next. Attempts still being checked two minutes into the wait are counted as undecided, and Playground says so.

> [!NOTE]
> You can start 10 runs an hour, and one at a time per simulation. A new run first removes the last run's members, so every run starts fresh. If they can't be removed, for example because one of their completions spent a budget the whole project shares, the run stops and says so: choose **Simulate** to start a new simulation and run it there.

> [!DEV]
> **Run** sends `POST /management/v1/playground/simulations/:simulation/runs` with the published `build`, `days` (1–30, default 7), `membersPerPersona` (default 5) and an optional `actionId`, from a Console session only. It answers `202` with the run; `GET .../runs/:run` reads its `status` (`running`, `completed` or `failed`), `daysDone`, `personas` and, once completed, `results`. A plan with more than 500 members is refused with `validation-failed`, a second run while one is running with `conflict`, and a build that hasn't published to the simulation with `conflict`. A retry with the same `actionId` answers the run it started. Members are test members of the simulation's project, acting through the same test-member operations as `domino test-members`. A sampled author's member is linked with `{ kind: "x", subject }`, their numeric X user ID, as `domino test-members link-account --kind x --subject` does, and their posts are sent as its records. `results.x` holds the samples, `authors`, `authorsPaid`, `posts` and `reads`, the X reads a day per tracker and plan; it's `null` without an X search tracker. The `x-authors` persona's tally is what the replayed posts earned.

## Make this real [#make-this-real]

When the simulation shows **Published**, **Make this real** publishes the same definitions to a project of yours.

1. Under **Make this real**, choose the **Project**. The list shows the projects in this organization you can publish to.
2. Choose **Make this real**, check the summary, and choose **Publish to test**.
3. Choose **Open deployments** to see the new deployment in that project.

The definitions are published to the project's **Test** environment the same way a deploy publishes them: a quest or ledger with the same key is updated, and the rest of the project stays as it is, including which kinds of quest its app shows and its collections. The simulation's members, points and records stay behind. To take the campaign live, [promote](/docs/concepts/promotion) it from **Deployments** as usual.

If the campaign has a quest of a kind the project's app doesn't show, such as a quiz, publishing is refused and nothing changes. Add support for it to the app first, or take that quest out and simulate again.

> [!DEV]
> Make this real sends `POST /management/v1/playground/simulation-copies` with the simulation and build IDs, selecting the target project and the `test` environment. It needs a Console session and publish permission in that project's **Test**; a live environment is refused with `test-environment-required`. It answers the new deployment's ID. It publishes like `domino deploy --only`: the project keeps its own `supportedInteractions` and collections, and a quest whose interaction they leave out is refused with `validation-failed`. A retry with the same `actionId` answers the first publication.
