Scenarios
A working tour of every behavior atjam can express — each one a live round on the network, not a mockup. Read top to bottom: a round is born, opens, runs, and closes; people join under different trust models; and a round carries just enough structure to stay generic.
Every round here is a real record on the network. It opens with a jam running in production, then walks every behavior through a deliberate test set on @nls-testuser1.bsky.social’s PDS, created by pnpm populate. Click any to open it.
In the wild: EPTSS
Not a fixture — this is EPTSS (“Everyone Plays the Same Song”) running live on atjam, owned by @everyoneplaysthesamesong.com. Each round names a song as its subject; participants submit a cover (an fm.plyr.track). Same primitives, same transit line as the test rounds below — see how it maps onto the extension points.
- createdApr 1, 2020
- signup-opensApr 1, 2020, 7:00 AM (passed)
- voting-opensApr 20, 2020, 7:00 AM (passed)
- covering-beginsApr 27, 2020, 7:00 AM (passed)
- submission-deadlineMay 11, 2020, 7:00 AM (passed)
- closing-eventMay 19, 2020, 7:00 AM (passed)
- now — closed
- createdApr 1, 2020
- signup-opensApr 1, 2020, 7:00 AM (passed)
- voting-opensApr 20, 2020, 7:00 AM (passed)
- covering-beginsApr 27, 2020, 7:00 AM (passed)
- submission-deadlineMay 11, 2020, 7:00 AM (passed)
- closing-eventMay 19, 2020, 7:00 AM (passed)
- now — closed
- createdMay 27, 2026
- signup-opensOct 5, 2025, 3:12 AM (passed)
- voting-opensFeb 2, 2026, 8:00 AM (passed)
- covering-beginsFeb 9, 2026, 8:00 AM (passed)
- closing-eventApr 23, 2026, 6:59 AM (passed)
- now — open
- createdMay 27, 2026
- signup-opensOct 5, 2025, 3:12 AM (passed)
- voting-opensFeb 2, 2026, 8:00 AM (passed)
- covering-beginsFeb 9, 2026, 8:00 AM (passed)
- closing-eventApr 23, 2026, 6:59 AM (passed)
- now — open
Reads open — the timeline has no submission-deadline, so deriveState never closes it even though every milestone has passed: the documented “keep a submission-deadline” gotcha, captured live.
The life of a round
A round has no status field — its state is derived from where now falls among its milestones. Open before the signup deadline, in-progress between the deadlines, closed once submissions close. Same record shape; watch the red now dot sit in a different stretch of track in each.
- createdJun 3, 2026
- now — open
- signup-deadlineJun 10, 2026, 3:10 AM (in 3 days)
- submission-deadlineJul 3, 2026, 3:10 AM (in 26 days)
- createdJun 3, 2026
- now — open
- signup-deadlineJun 10, 2026, 3:10 AM (in 3 days)
- submission-deadlineJul 3, 2026, 3:10 AM (in 26 days)
- createdJun 3, 2026
- signup-deadlineJun 2, 2026, 3:10 AM (passed)
- now — in-progress
- submission-deadlineJul 3, 2026, 3:10 AM (in 26 days)
- createdJun 3, 2026
- signup-deadlineJun 2, 2026, 3:10 AM (passed)
- now — in-progress
- submission-deadlineJul 3, 2026, 3:10 AM (in 26 days)
- createdJun 3, 2026
- signup-deadlineMay 27, 2026, 3:10 AM (passed)
- submission-deadlineJun 2, 2026, 3:10 AM (passed)
- now — closed
- createdJun 3, 2026
- signup-deadlineMay 27, 2026, 3:10 AM (passed)
- submission-deadlineJun 2, 2026, 3:10 AM (passed)
- now — closed
Naming the phases
Milestones are an open list. Past the two standard deadlines you can add as many named phases as you like — each becomes another station, and deriveCurrentPhase reports which stretch now is in. The catch: state only closes on a submission-deadline, so a round built from custom labels alone reads OPEN forever.
- createdJun 3, 2026
- signup-opensMay 4, 2026, 3:10 AM (passed)
- voting-opensMay 14, 2026, 3:10 AM (passed)
- covering-beginsMay 24, 2026, 3:10 AM (passed)
- closing-eventJun 1, 2026, 3:10 AM (passed)
- now — open
- createdJun 3, 2026
- signup-opensMay 4, 2026, 3:10 AM (passed)
- voting-opensMay 14, 2026, 3:10 AM (passed)
- covering-beginsMay 24, 2026, 3:10 AM (passed)
- closing-eventJun 1, 2026, 3:10 AM (passed)
- now — open
Reads open — the timeline has no submission-deadline, so deriveState never closes it even though every milestone has passed: the documented “keep a submission-deadline” gotcha, captured live.
- createdJun 3, 2026
- signup-opensMay 24, 2026, 3:10 AM (passed)
- voting-opensMay 29, 2026, 3:10 AM (passed)
- covering-beginsJun 2, 2026, 3:10 AM (passed)
- now — open
- submission-deadlineJun 23, 2026, 3:10 AM (in 16 days)
- closing-eventJun 28, 2026, 3:10 AM (in 21 days)
- createdJun 3, 2026
- signup-opensMay 24, 2026, 3:10 AM (passed)
- voting-opensMay 29, 2026, 3:10 AM (passed)
- covering-beginsJun 2, 2026, 3:10 AM (passed)
- now — open
- submission-deadlineJun 23, 2026, 3:10 AM (in 16 days)
- closing-eventJun 28, 2026, 3:10 AM (in 21 days)
Who's allowed in
Three trust models, encoded by form rather than colour (colour is reserved for state). A signup cites the invitation that admitted it; a read-time validator then walks the chain and separates legitimate from spoofed — open any round to see the green / amber badges.
- open
- Anyone may sign up — no invitation needed.
- hosted
- The organizer must invite you directly.
- network
- Invitations chain, but every chain must root at the organizer.
- Network round — cycle attemptopen
joinMode=network. test2 and test3 try to invite each other without ever rooting at the organizer (no organizer→test2 or organizer→test3 invitation exists). Both signups cite the other's invitation. Validator should detect the cycle and render both as INVALID with the 'cycle detected' reason.
- Network round — contributed gateopen
joinMode=network, networkGate=contributed. Two inviters: • test2 has signup AND submission → her invitee (test3) is VALID • test4 has signup but NO submission → her invitee would be INVALID (test4 didn't invite anyone, but we set it up so validator's gate logic is visible per chain) Confirm the validator distinguishes inviters who've earned invitation rights from those who haven't.
- Network round — chainopen
joinMode=network. Chain of invitations: organizer → test2 → test3, plus organizer → test4 directly. All three signups should render valid because every chain roots at the organizer.
- Hosted round — mixed validationopen
joinMode=hosted. Three signups, three different validator outcomes: • test2 with valid invitation from organizer → VALID • test3 with no invitation field → INVALID • test4 with self-forged invitation → INVALID Hover the amber badges to see the failure reason.
What a round carries
A round stays generic on purpose. Domain specifics ride optional, $type-tagged extension points — a subject and a closingEvent — plus a list of accepted submission types. atjam never learns what your domain is.
- Round accepting multiple submission typesopen
acceptedSubmissionTypes lists three NSIDs from different atproto apps. The 'Accepted submission types' section should render all three as pills.
- Round with subject + closingEventopen
Both optional structured fields (subject and closingEvent) are populated with fake $type-discriminated objects. The page should render without crashing even if it doesn't display the structured fields directly.
The quiet baseline
A fresh round with nobody in it yet — proof the zero case is drawn as deliberately as the full one.