Imported from hpcsc/dotfiles (
link/common/dot-config/.config/opencode/skills/draw-event-flow/SKILL.md). Install upstream withnpx skills add hpcsc/dotfiles --skill draw-event-flow. Copyright stays with the author.
Draw the event flow for: $ARGUMENTS
This borrows two symbols from Event Modeling — the orange event and the gear automation — and drops everything else about it. There are no swimlanes, no timeline axis, and commands and read models appear only if the reader needs them. What the diagram shows is: an event arrives, an automation reacts, and one or more events come out, with the condition on each emission named on the edge.
Produce both deliverables every time, from the same model:
- A standalone
.svg— self-contained, light/dark aware, opens in any browser and embeds in a README, Confluence or a doc. Hand-placed, so the layout is deliberate — unless the system carries an.emodmodel, which draws its own (see below). - A Mermaid
.mmd— auto-laid-out, easy for someone else to edit, plus a rendered.pngproving it parses. An.emodmodel writes this one too (see below).
Start from the templates, which carry the palette, the gear symbol and the parser workarounds already:
~/.config/ai/method/draw-event-flow/templates/event-flow.svg~/.config/ai/method/draw-event-flow/templates/event-flow.mmd
Get the facts before drawing
A diagram that is merely plausible is worse than none: readers trust pictures more than prose. Before drawing, confirm from the code, not from naming:
- Which events each automation actually subscribes to. Read the handler map or the subscription filter policy. An event a diagram shows arriving somewhere it does not is the most damaging kind of error.
- What it emits, and under what condition. Distinguish "emits X" from "emits X only when Y".
- Whether an emission can happen without its apparent trigger, and whether two events are emitted in one write — those need drawing as a pair, not as siblings.
- Which edges have no gate at all. These matter most; readers assume a gate exists, so mark the hops that have none.
- Dead ends: an event nothing consumes. Say so, or the reader will assume something reacts to it.
- Whether each thing you are about to draw as an event is one. A pill claims the log holds that fact. A message from a scheduler or a queue, a timer that fires, a vendor's response and a write to an external store are none of them events, however event-shaped their names are. Check that something appends it before it earns a pill.
When the system carries an .emod model, read the model, not the code. It states every subscription outright, which is the guesswork this section exists to remove. emod diagram <model>.emod --format event-flow writes the first deliverable from it — pills, bare gears, external boxes, and an event nothing reads drawn dashed — and --format event-flow-mermaid writes the second, with the ELK layout and the parser workarounds already in place. Look at both before hand-placing anything, and hand-place only what neither draws. emod export <model>.emod --format json hands over the same facts as data: each automation's on, its after or every, the command it issues, and the flow naming the event that command emits.
The model has one blind spot, and it is this diagram's subject: the gate. emod states a refusal as an invariant a command meets, never as a feature toggle or a runtime flag, so a condition on an emission still comes from the code. It leaves views out too, so an event only a read model consumes shows no arrow onward and no dead-end mark.
Show the mechanism, not its name. A box labelled "cache" says less than the prose; the path a request takes through it says what the words cannot.
The visual grammar
| Element | Shape | Light | Dark |
|---|---|---|---|
| Event | Stadium pill, rx = half the height |
fill #FFE0C0, stroke #B44E12 |
fill #40260F, stroke #F2A868 |
| Automation | Bare gear, no box, name captioned below | fill #16201B |
fill #E2E9E5 |
| Gated edge | Stroke 2px, label = the gate's name | #14705A |
#53C3A1 |
| Plain edge | Stroke 1.4px — "this event is consumed by that" | #16201B |
#E2E9E5 |
| Store or external service | Rounded box, rx 8, name inside |
fill #ECEFED, stroke #5A6861 |
fill #1E2723, stroke #93A199 |
| No gate / dead end | Red text, or a dashed red stroke on a pill | #A33A2A |
#E88C78 |
| An emission that never happens | Dashed red box, never a pill | stroke #A33A2A on the background fill |
stroke #E88C78 |
Only a fact in the log gets a pill. The shape is a claim, and a reader who sees a pill believes the system recorded that fact and can replay it. Everything else that moves through the picture takes another form: a scheduler's message, a queue delivery or a vendor call is an edge with a label, and the thing that holds it is a store box. When the moment genuinely needs to be auditable, the fix is in the system rather than in the drawing: append a real event, and then the pill is true.
An absence is not an event either. "The automation emits nothing here" is the point of many diagrams, and a dashed pill says the opposite, because a pill of any stroke still reads as a fact. Draw it as a dashed red box, and keep the dashed pill for its own case: a real event that nothing consumes.
The orange is Event Storming's event colour and is the one convention worth keeping exactly. Event Modeling also uses blue for commands and green for read models — do not introduce those unless the reader needs them, and never reuse blue or green for anything else in the same picture.
The automation is a bare gear, not a box with a gear in it. Losing the box is the point: it is what makes the picture read as an event model rather than a flowchart.
The standalone SVG
The gear is defined once in <defs> and placed with <use>:
<g id="gear" class="gear">
<circle cx="0" cy="0" r="8.4"/>
<rect x="-2.9" y="-14.2" width="5.8" height="6.8" rx="1.3"/>
<!-- ...the same rect at rotate(45) through rotate(315)... -->
<circle cx="0" cy="0" r="3.4" class="gear-hole"/>
</g>
...
<use href="#gear" transform="translate(465,48)"/>
<text x="465" y="78" text-anchor="middle" class="name">TheAutomation</text>
Eight teeth at 45° increments. The hub hole is filled with the background colour and needs its own dark override, or it turns into a solid dot in dark mode.
Write every colour as a literal, never a CSS custom property. Non-browser renderers — librsvg, Quick Look, Inkscape, most doc previews — do not resolve var(), and every fill that fails to resolve draws black, so the whole diagram vanishes into its background. A browser resolves it fine, which is why the bug stays hidden until someone opens the file anywhere else. Give each class its light-theme literals, then re-declare only the colour properties per class under @media (prefers-color-scheme: dark). A renderer that ignores the media query shows the light theme, which is the right fallback.
Rules that keep it readable:
- Give every text label a halo so it can sit on an edge:
paint-order: stroke; stroke: #FFFFFF; stroke-width: 5px, with the stroke overridden to the dark background under the media query. Without it the whole layout becomes an exercise in dodging lines. - A halo only masks what is drawn before it. SVG paints in document order, so an edge declared after a label is drawn over it, halo or not. Declare labels after the edges they sit on — or move the label along its own edge until nothing else crosses it.
- Route edges in channels, not diagonals, when several leave one automation. Pick a distinct x for each elbow and check by arithmetic that no channel crosses a node or a label — a channel through a caption is the usual failure.
- Captions sit below the gear, about 30px down, with any sub-line 15px below that. Aim arrows at the gear, not at where a box would have been.
- Leave room in the
viewBoxfor the outermost labels, including captions that hang below their node.
Hand-placed SVG has no layout engine to catch a collision, so check the arithmetic on every elbow before rendering.
The Mermaid source
Five things decide whether it is readable, and four of them are not obvious.
Use ELK, not the default renderer. This is the single biggest difference between a clean diagram and a tangle:
---
config:
layout: elk
securityLevel: loose
elk:
mergeEdges: false
nodePlacementStrategy: BRANDES_KOEPF
---
Keep edge labels to two short lines. Mermaid lays out edge labels as objects, so a four-line label inflates the ranks and forces long detours. Put the gate's name and at most one qualifier on the edge; everything else goes in a comment block at the top of the file.
Use flowchart TB. LR on a chain of this shape produces a technically perfect ribbon — 2400×150 — that is unusable in a document.
The gear is fa:fa-cog sized with an inline span, on a node styled fill:none,stroke:none:
automation["<span style='font-size:30px'>fa:fa-cog</span><br/>TheAutomation"]
classDef gear fill:none,stroke:none,color:#16201B
Font Awesome is bundled in mermaid-cli and mermaid.live, and securityLevel: loose is what allows the span. GitHub has neither and prints the literal text fa:fa-cog. If the file is destined for a README, swap every span for a plain unicode ⚙ — portable everywhere, just thinner. Record which you chose in a comment at the top.
Three parser traps, each of which costs a render:
- A
%%comment block between the frontmatter andflowchartfails withExpecting 'NEWLINE', 'SPACE', 'GRAPH', got 'NODE_STRING'. The diagram type must come first; comments go after it. - A bare
%%line inside the body renders as a visible node labelled%%. Use%% ---as a separator. @{ icon: "fa:cog" }, the syntax the Mermaid docs recommend for icons, renders a blue box with a ? — it needs an iconify pack registered at runtime, whichmermaid-clidoes not do. Do not use it.
Also: :::class ends a statement, so declare nodes and edges on separate lines rather than chaining a class onto an edge; HTML entities render literally, so use the actual character; and linkStyle indexes are 0-based in declaration order, so recount them after inserting an edge.
Render and look
mermaid-cli is installed under mise and is not on PATH:
~/.local/share/mise/installs/npm-mermaid-js-mermaid-cli/*/bin/mmdc -i flow.mmd -o flow.png -b white -w 1600
Render the Mermaid before showing it to anyone — it is the only way to know it parses, and a silent layout disaster looks identical to success in the source. Look at the output once, fix what it shows in a single pass, and stop. Do not build a screenshot loop.
For the SVG, render it with a non-browser engine, not a browser — a browser resolves CSS custom properties and hides exactly the failure that matters:
rsvg-convert -w 1300 flow.svg -o /tmp/flow-check.png
Then read the PNG back and look. A black rectangle means a colour is still a var(). Check both themes if the reader will see both.
Where the files go
Unless the user names a location, write to ./.scratches/<slug>/ in the current repo, grouping the files together:
.scratches/<slug>/
<name>.svg the standalone SVG
<name>.mmd the Mermaid source
<name>.png the rendered Mermaid, as proof it parses
If the diagram belongs in committed documentation, put it where that documentation lives and prefer the .svg for embedding.
Report
Say what the diagram claims, not how it was made. Name the facts verified in the code and any that could not be, so the reader knows which parts to trust. If the two versions differ in any way beyond layout — a label shortened, a node dropped — say so.