# Campaigns (/docs/concepts/campaigns)



A campaign groups quests that run together, such as a launch month, a holiday event or a hackathon week. The quests share one set of rules:

* **The same dates.** Every quest in the campaign runs within the campaign's dates, and a quest can run for a shorter stretch inside them.
* **An optional sign-up.** You can ask members to join the campaign before any of its quests count.
* **Shared limits.** Quests can draw from one shared budget, such as 1,000 completions across the whole campaign.

In Console, open a quest under **Quests**: its **How it works** summary shows its campaign, whether the campaign requires enrollment, and the campaign and quest dates that apply. They come from code, so your developer changes them there.

> [!DEV]
> Define a campaign once in code, as below, and attach the same definition to each quest that belongs to it. Campaign keys and definitions must agree across every active quest that uses them. A quest can add its own `availability` window inside the campaign window. Both intervals are half-open: the start is included and the end is excluded. Publication refuses a quest window that does not overlap its campaign.

```ts
import { defineCampaign } from "@domino-sdk/relay/authoring";

export const launch = defineCampaign({
  key: "launch",
  title: "Launch campaign",
  window: { from: Date.parse("2026-10-01T00:00:00Z"), until: Date.parse("2026-11-01T00:00:00Z") },
  enrollment: "required",
});
```

## Enrollment [#enrollment]

A campaign that requires enrollment counts a member's activity only after they join it, for example with a **Join the launch** button. Domino remembers when each member first joined, for every current and future quest in the campaign.

Members can leave a campaign, and your team can take them out of it in Console, with a reason. Leaving doesn't ban anyone: joining again restores their place and keeps their first join time.

| While a member is out   | What happens                                                                                               |
| ----------------------- | ---------------------------------------------------------------------------------------------------------- |
| New activity            | Not counted: no new quest tries start, whether submitted, automatic or from a tracker.                     |
| Tries already under way | Continue: they can still be retried, reviewed, completed and rewarded.                                     |
| Completions and rewards | Stay.                                                                                                      |
| Rejoining               | Lets new activity count again, and looks again at activity that was skipped because they weren't enrolled. |

Console shows each member's first join time, and when and why they left, and lets someone with permission take them out or restore them. You can't join a campaign that doesn't require enrollment, or one that has ended. A member you [erased](/docs/use/member-erasure) can't be enrolled again; their enrollment history stays.

> [!DEV]
> Set `enrollment: "required"` on the campaign. The member calls `relay.campaigns.enroll({ actionId, campaign })` and `relay.campaigns.withdraw({ actionId, campaign })`. A trusted server or Console operator calls `management.campaigns.enroll({ actionId, campaign, member })` and `management.campaigns.withdraw({ actionId, campaign, member, reason })` with `enroll` permission. Repeating enrollment returns the first time; enrolling again after withdrawal restores the enrollment without changing `enrolledAt`. Attempts admitted before withdrawal keep their pinned release. Restoration decides prior `not-enrolled` skips again.

A quest's `evaluate` reads the member's enrollment with `ctx.member.enrolled(campaign)`: `true` while it is active, and `false` after withdrawal or without an enrollment. See [evaluator readers](/docs/reference/verification#evaluator-readers-null-errors-and-pinning).

## Quest eligibility [#quest-eligibility]

Your app can tell each member what they can do with a quest right now: start it, finish a prerequisite first, wait for it to open, or wait for a review.

> [!DEV]
> `QuestView.availability` says what can happen now:
>
> | Shape | Meaning |
> | --- | --- |
> | `{ kind: "available", input }` | The member can submit. |
> | `{ kind: "locked", missing }` | One or more typed prerequisites are missing. |
> | `{ kind: "blocked", reason, resetsAt? }` | A window, enrollment, pause, connection, wallet, or budget rule blocks participation. |
> | `checking`, `needs-review`, `retryable` | An admitted attempt owns the next action. |
> | `automatic`, `staff-required`, `completed` | Relay, staff, or the completed state owns the next action. |
>
> The view also includes the effective `window`, campaign definition, `enrolledAt`, `withdrawnAt`, and `withdrawalReason`, and each authored budget's current `remaining` capacity and `resetsAt`. `questStatus(view)` returns stable codes for localization; `questStatusText(view)` supplies optional English labels.

What a member can do changes over time without rewriting their history. Point and group prerequisites read the member's current balance and groups. "Once a day" prerequisites and budget resets follow the [project calendar](/docs/concepts/calendar). A try already under way keeps its rules; a later change to the member's balance, groups, the dates or a budget doesn't rewrite it.

## Campaign-wide budgets and rankings [#campaign-wide-budgets-and-rankings]

Quests share a campaign budget when each uses the same budget, with the same scope, period and limit. Declare the budget once and use it in each quest: Domino refuses to publish two quests that declare one budget differently. It enforces the limit at the moment each completion is recorded, so it can't be overshot. Day, week, and month budgets follow the project calendar.

A leaderboard can count only the campaign's quests, and can cover the current campaign day, week, or month. Being in a campaign doesn't award points or prizes by itself.

> [!DEV]
> Use the same budget ID, scope, period, limit and cost on each quest; declare it once and import it. Deployment refuses one budget ID declared differently, and `domino check` and `domino build` report it already when the values are literals. The budget is enforced atomically at completion. Filter a leaderboard to the campaign's quests with an earned-score `quests` filter, and use a calendar leaderboard window for standings per day, week, or month.

## Where next [#where-next]

- [Quests](/docs/concepts/quests) — Prerequisites and how often a quest can be completed.
- [Compose a campaign](/docs/composition) — Connect campaign quests, points, referrals, and standings.
- [Participant API](/docs/reference/participant-api) — Render availability and enroll a signed-in member.
