Events and Commands
Events are facts. Commands are requests. They are different shapes of the same idea — something needs to be communicated through the runtime — and using the right one keeps causality readable.
Events
Section titled “Events”runtime.emit({ type: 'note.saved', payload: { id: '42' } })An event says: this happened. It has no return value. Many modules may observe it. Each handler runs independently. Failures in one handler don’t affect others.
Common event types:
note.savednote.deleteddraft.changedform.validation_faileddialog.openedroute.changedsession.expiredcheckout.startedNotice the past tense (or neutral) phrasing. Events describe state that already changed.
Commands
Section titled “Commands”const result = await runtime.command({ type: 'notification.show', payload: { message: 'Saved', tone: 'success' },})A command says: please do this. It has exactly one handler. It returns a result. Failures are caught and reported via the result.
Common command types:
notification.showdialog.opendialog.closedraft.saveform.validateclipboard.copyroute.navigateNotice the imperative phrasing. Commands ask for behavior.
Why separate them
Section titled “Why separate them”You could imagine a runtime with only one channel — every message is both an event and a command. Some frameworks do this. The cost is that you can’t tell at a glance whether sending the message is observation or imperative.
Kitsune separates them because:
- Events have many observers; commands have one handler. Conflating those puts ordering pressure on observation that observation shouldn’t have.
- Events don’t return values; commands do. A submit handler that needs to know “did the form validate?” is asking a command. A toast pop that needs no answer is observing an event.
- Events should not have side effects beyond observation. If your “event handler” is doing the work the user expects, it should probably be a command handler.
A useful test: if I removed every observer of this message, should the user-visible behavior still work? Yes → command. No → event.
Combining them in one interaction
Section titled “Combining them in one interaction”Many user actions emit a fact and request behavior at the same time. The metadata protocol supports both:
<kit-button meta-event="settings.saved" meta-command="dialog.close" meta-prop-target="settings-dialog"> Save</kit-button>Click the button. The runtime:
- Emits the
settings.savedevent (audit, analytics, etc. observe it). - Dispatches the
dialog.closecommand (the dialog module closes the dialog).
Event-first ordering means observers see the fact before the command runs. If the command updates state, observers can choose whether to react before or after.
Naming events and commands
Section titled “Naming events and commands”A loose convention that scales:
Events: entity.action_in_past_tense
note.savednote.deletedcheckout.startedcheckout.completedform.validation_failedsession.timed_outCommands: domain.imperative
notification.shownotification.dismissdialog.opendialog.closeclipboard.copydraft.saveform.validateroute.navigateUse namespaces (note., dialog., route.) so observers can subscribe to whole families:
runtime.on('note.saved', auditNote)runtime.on('note.deleted', auditNote)// orruntime.on('*', everything)Wildcard subscriptions are a debug convenience; production observers should usually pick specific types.
Payloads
Section titled “Payloads”Both events and commands accept a payload object. Keep payloads:
- Flat. Avoid nested objects when a flat shape would do.
- Serializable. Strings, numbers, booleans, arrays, and plain objects only. No DOM nodes, no functions, no class instances. Future devtools and replay tools will assume this.
- Small. A payload isn’t a database. Reference entities by id; let the module fetch what it needs.
Payloads from DOM metadata are always strings (because attributes are strings). Programmatic payloads can be richer.
Context
Section titled “Context”Both events and commands carry a context from the surrounding boundary. You don’t pass context manually for DOM-driven interactions — the boundary adds it automatically.
When emitting programmatically, the runtime exposes the current boundary context if you have a handle to it; usually you don’t need to set context manually.
Failures
Section titled “Failures”A command’s handler can throw or return a rejected promise. The runtime wraps it:
const result = await runtime.command({ type: 'draft.save', payload: { id: '42' } })
if (!result.ok) { console.error('save failed:', result.error)} else { console.log('saved at', result.value)}Event handlers that throw are isolated — one bad handler doesn’t break the rest. The runtime emits a diagnostic so you see the failure.
Read next
Section titled “Read next”- Modules — who handles events and commands
- The Metadata Protocol — how UI declares them
- Reference: Runtime API