From tl
Prepares a participant's domain findings before a multi-human Event Storming session. Agents explore a bounded context and output candidate domain events, aggregates, and boundary assumptions as structured YAML.
How this skill is triggered — by the user, by Claude, or both
Slash command
/tl:storm-prep <domain scope and project goal><domain scope and project goal>This skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
You are running the **Storm-Prep** workflow — Phase 1 of a multi-human Event Storming trilogy. Each human's Claude agents independently explore their assigned domain and produce candidate domain events as structured YAML, ready for a human jam session and for downstream formalization. Domain scope: **$ARGUMENTS**
You are running the Storm-Prep workflow — Phase 1 of a multi-human Event Storming trilogy. Each human's Claude agents independently explore their assigned domain and produce candidate domain events as structured YAML, ready for a human jam session and for downstream formalization. Domain scope: $ARGUMENTS
/formalize (Phase 2) and a cross-human reconciliation session (Phase 3)/spec or /decompose insteadevents/candidates/{role}.yamlThe YAML output conforms to the JSON Schema defined in candidate-events.schema.json (bundled with this skill). The schema enforces:
role, scope, goal, generated_at (ISO 8601), event_count, assumption_countname/aggregate, trigger, typed payload fields, integration with direction enum (inbound/outbound/internal), confidence (CONFIRMED/LIKELY/POSSIBLE)BA-N id pattern, type enum (ownership/contract/ordering/existence), statement, affects_events, verify_withadditionalProperties: false at every level — no unstructured driftCopy the schema into the project before first use:
mkdir -p events/candidates
cp <tackline>/skills/workflows/storm-prep/candidate-events.schema.json events/candidates/schema.json
Validate output:
uv run --with check-jsonschema -- check-jsonschema --schemafile events/candidates/schema.json events/candidates/{role}.yaml
Storm-prep works in 4 phases. The scope gate (Phase 0) must pass before exploration begins.
Scope gate (project goal + domain scope defined)
-> Domain exploration (agents fan out across codebase/goal)
-> Assumption surfacing (what this human assumes at boundaries)
-> YAML output generation (events, aggregates, assumptions, confidence)
-> Schema validation (check-jsonschema against candidate-events.schema.json)
-> events/candidates/{role}.yaml — consumed by jam session + /formalize
Law: NO EXPLORATION WITHOUT SCOPE. This phase is a hard gate. If either check fails, stop here.
If $ARGUMENTS is empty or does not include both a domain scope and a project goal, ask:
"What is your domain scope (the area you own in this session) and the overall project goal? Example: 'scope: order fulfillment, goal: rebuild checkout system'"
Do not proceed until both are provided.
Extract a role identifier from the scope. Use the domain name in kebab-case (e.g., order-fulfillment, user-auth, inventory). This becomes the output filename: events/candidates/{role}.yaml.
State the role identifier explicitly: "Role identified: {role}. Output will be written to events/candidates/{role}.yaml."
If events/candidates/{role}.yaml already exists, read it and display the timestamp from the generated_at field. Ask:
"A prior storm-prep run exists for this role (generated: {timestamp}). Overwrite it? (yes / no)"
If the user says no, exit. If yes, proceed.
## Scope Gate: [PASS | FAIL]
- Domain scope: [{scope}]
- Project goal: [{goal}]
- Role identifier: [{role}]
- Output path: [events/candidates/{role}.yaml]
- Prior output: [none | found — {timestamp}, overwriting]
[If FAIL]: Define scope and goal before running storm-prep.
[If PASS]: Proceeding to domain exploration.
Dispatch 3 Explore agents concurrently to investigate the domain from different angles. Each agent looks for candidate domain events — things that happened in the system, expressed as past-tense verbs (e.g., OrderPlaced, PaymentFailed, InventoryReserved).
You are a domain event explorer for an Event Storming session.
Project goal: {goal} Domain scope: {scope}
Your job: Discover candidate domain events by tracing what commands (user actions, API calls, UI triggers) cause state changes in this domain.
Your perspective: You think in terms of cause and effect — what actions users or systems take, and what observable outcomes result. You look for command handlers, API endpoints, service methods, or workflow steps that write state. For each, you surface the domain event it produces. Events are named as past-tense verbs:
UserRegistered,OrderCancelled,PaymentProcessed.Investigation protocol:
- Use Glob and Grep to find command handlers, API routes, service methods, or controllers in this domain
- For each, read the implementation to understand what state change it produces
- Name the domain event as a past-tense verb (PascalCase)
- Identify the aggregate (the entity whose state changes) and any data payload
- Verify by checking if tests or consumers reference this state change
- Distinguish CONFIRMED (code verified), LIKELY (strong evidence), POSSIBLE (inferred)
Report format:
## Command-Flow Domain Events ### {EventName} - aggregate: {EntityName} - trigger: {command or action that causes this} - payload: [{field}: {type}, ...] - source: {file:line} - confidence: CONFIRMED | LIKELY | POSSIBLE [Repeat for each event found]Report up to 15 events. Prioritize events with the clearest implementation evidence.
You are a domain event explorer for an Event Storming session.
Project goal: {goal} Domain scope: {scope}
Your job: Discover candidate domain events by tracing state transitions — what database writes, entity mutations, or persistence operations happen in this domain.
Your perspective: You think like a database archaeologist. You look for INSERT/UPDATE/DELETE operations, ORM save/create/update/delete calls, event store writes, or any persistence layer mutations. Each meaningful state change is a candidate domain event. You surface what transitions are happening under the hood, even when the code doesn't name them as events explicitly.
Investigation protocol:
- Use Grep to find database writes (ORM methods, raw SQL, event store appends) in this domain's files
- For each write, trace back to the domain concept whose state changes
- Name the implied domain event (PascalCase past-tense verb)
- Identify the aggregate and the fields that change
- Check if there are existing event classes, event types, or message schemas that already name this event
- Distinguish CONFIRMED (explicit event type found), LIKELY (state change clear but unnamed), POSSIBLE (inferred from data shape)
Report format:
## State-Transition Domain Events ### {EventName} - aggregate: {EntityName} - state_change: {before state} -> {after state} (or: "new entity created" / "entity deleted") - existing_type: {class or message type if named in code, or "none"} - source: {file:line} - confidence: CONFIRMED | LIKELY | POSSIBLE [Repeat for each event found]Report up to 15 events. Prefer events with explicit existing event types.
You are a domain event explorer for an Event Storming session.
Project goal: {goal} Domain scope: {scope}
Your job: Discover candidate domain events at integration boundaries — what does this domain publish to the outside world, and what does it consume from other domains?
Your perspective: You think like a systems integrator. You look for message buses, event queues, webhooks, notification systems, or inter-service calls that carry state transitions. Integration points reveal domain events that matter enough to cross a boundary — these are often the most important events in the model.
Investigation protocol:
- Use Grep and Glob to find message publishers, event emitters, queue producers, webhook dispatchers, or outbound HTTP calls in this domain's files
- For each integration point, identify what domain event is being communicated
- Also find message consumers, event handlers, or queue subscribers that receive events from other domains — these reveal boundary assumptions
- Name the published events (PascalCase past-tense) and label inbound events as "consumed from {other domain}"
- Distinguish CONFIRMED (explicit message type found), LIKELY (integration code found but type inferred), POSSIBLE (integration suspected from naming patterns)
Report format:
## Integration-Boundary Domain Events ### {EventName} [published | consumed from {domain}] - aggregate: {EntityName} - direction: outbound | inbound - channel: {message bus, queue name, webhook endpoint, or service call} - source: {file:line} - confidence: CONFIRMED | LIKELY | POSSIBLE [Repeat for each event found]Report up to 10 events per direction. Integration events often have higher confidence than internal events.
Launch all 3 agents with run_in_background: true. Wait for all to complete before proceeding to Phase 2.
Task({
subagent_type: "Explore",
run_in_background: true,
prompt: "<Agent 1 prompt with {goal} and {scope} substituted>"
})
Task({
subagent_type: "Explore",
run_in_background: true,
prompt: "<Agent 2 prompt with {goal} and {scope} substituted>"
})
Task({
subagent_type: "Explore",
run_in_background: true,
prompt: "<Agent 3 prompt with {goal} and {scope} substituted>"
})
Merge the three agent reports into a deduplicated event list. When the same event appears in multiple reports:
Produce a working list: N_total candidate events from N_command command-flow, N_state state-transition, N_integration integration-boundary findings.
Before generating output, make boundary assumptions explicit. These are what this human believes about adjacent domains — beliefs that other participants may contradict in the jam session.
For each event that crosses a domain boundary (inbound or outbound), or for aggregates that reference entities outside this scope, surface the assumption:
{Entity} is owned by {other domain}"{EventName} carries {field} in its payload"{EventA} always precedes {EventB}"{command or process} belongs to this domain, not {adjacent domain}"Review the consolidated event list for any events where the domain ownership is ambiguous or where you made an implicit modeling decision. Flag these as assumption_type: ownership.
Each assumption gets a confidence level:
Generate the structured YAML artifact. Create events/candidates/ if it does not exist.
The YAML must conform to candidate-events.schema.json. Use only the fields defined in the schema — additionalProperties: false is enforced at every level.
# events/candidates/{role}.yaml
# Generated by /storm-prep — Phase 1 of Event Storming workflow
# Consumed by: human jam session, /formalize skill
# Schema: events/candidates/schema.json (candidate-events.schema.json)
# Role: {role}
# Domain scope: {scope}
# Project goal: {goal}
# Generated at: {ISO 8601 timestamp}
metadata:
role: "{role}"
scope: "{scope}"
goal: "{goal}"
generated_at: "{timestamp}"
event_count: {N}
assumption_count: {M}
domain_events:
- name: "{EventName}"
aggregate: "{AggregateRoot}"
trigger: "{command or action}"
payload:
- field: "{fieldName}"
type: "{type}"
state_change: "{before} -> {after}" # omit if not applicable
integration:
direction: outbound | inbound | internal
channel: "{channel name}" # omit if internal
sources:
- "{file:line}"
confidence: CONFIRMED | LIKELY | POSSIBLE
notes: "{any modeling uncertainty or open question}" # omit if none
# ... one entry per candidate event
boundary_assumptions:
- id: "BA-{N}"
type: existence | contract | ordering | ownership
statement: "{assumption as a declarative sentence}"
affects_events:
- "{EventName}"
confidence: CONFIRMED | LIKELY | POSSIBLE
verify_with: "{which other role or domain should confirm this}"
# ... one entry per assumption
Write to events/candidates/{role}.yaml. Create the directory if needed.
After writing, validate against the schema:
Copy the schema if not already present:
cp <tackline>/skills/workflows/storm-prep/candidate-events.schema.json events/candidates/schema.json
If events/candidates/schema.json already exists, skip this step.
Run schema validation:
uv run --with check-jsonschema -- check-jsonschema --schemafile events/candidates/schema.json events/candidates/{role}.yaml
If validation fails, fix the YAML to conform. Common issues:
name, aggregate, trigger, payload, integration, confidence)name and aggregate must be PascalCase: ^[A-Z][a-zA-Z0-9]+$)direction must be inbound, outbound, or internal; confidence must be CONFIRMED, LIKELY, or POSSIBLE)additionalProperties: false is enforced everywhere)^BA-\d+$After validation passes, confirm counts:
event_count matches actual domain_events lengthassumption_count matches actual boundary_assumptions lengthState: "Validated: events/candidates/{role}.yaml passes schema check with {N} events and {M} assumptions."
Emit a pipe-format block so downstream skills (/formalize, /gather, /rank) can consume the findings.
Consuming skill detection: A downstream /formalize invocation should detect this output by looking for **Source**: /storm-prep in context. The events/candidates/{role}.yaml path in the Input line is the primary handoff artifact.
## Domain Events: {scope}
**Source**: /storm-prep
**Input**: {scope} / {goal}
**Pipeline**: (none — working from direct input)
### Items (N)
1. **{EventName}** — {aggregate}, {direction: internal | outbound | inbound}
- source: events/candidates/{role}.yaml
- confidence: CONFIRMED | LIKELY | POSSIBLE
2. **{EventName}** — ...
[... one item per domain event, ordered by confidence descending]
### Assumptions ({M})
1. **BA-1** — {statement}
- type: {existence | contract | ordering | ownership}
- verify_with: {other role}
- confidence: CONFIRMED | LIKELY | POSSIBLE
[... one item per boundary assumption]
### Summary
[One paragraph: total events found, primary aggregates identified, key boundary assumptions that need confirmation in the jam session, and any modeling uncertainties that surfaced during exploration.]
check-jsonschema against events/candidates/schema.json before the skill reports success. Do not skip validation.OrderPlaced not PlaceOrder, PaymentFailed not PaymentFailure.events/candidates/{role}.yaml. It does not modify other roles' files.confidence: POSSIBLE and flag them in boundary_assumptions. Let the humans decide in the jam session.candidate-events.schema.json (bundled with this skill) is the single source of truth for the YAML structure. If you need to add fields, update the schema first — never add unschematized fields to the YAML.POSSIBLE ownership assumption — do not silently assign it to this scope.PlaceOrder is a command. OrderPlaced is an event. If you find yourself naming events without past tense, you're modeling commands, not events.candidate-events.schema.json first. Unschematized fields will cause validation failure./formalize — Phase 2 of the trilogy: takes events/candidates/*.yaml from all participants and produces a unified domain model (aggregate map, bounded contexts, event flow diagram)/spec — use to design a single domain before or after Event Storming/decompose — break a large goal into sub-domains before running storm-prep per domain/meeting — facilitate the human jam session that follows storm-prep across participants/gather — feeds into storm-prep when the project goal needs research before domain explorationnpx claudepluginhub tyevans/tackline --plugin tacklineRuns event storming workshops to discover domain events, commands, and bounded contexts for system design, migration, or microservices decomposition.
Guides domain modeling using DDD strategic patterns: bounded contexts, context maps, subdomain analysis, and ubiquitous language. Useful for decomposing monoliths or designing microservices.