Outcome rules
A rule turns facts Morse already has into an outcome label for the runs of one agent. Two templates ship, coding and support. Each is a single time-based check, and each is configured per agent on Settings > Outcomes.
A rule label has the rule provenance. It shows the rule’s criterion and version, for example
support_ticket_resolved v3: ticket resolved, not reopened within 7d. It is an estimate, not a
human-confirmed outcome. See
Outcomes that fill themselves for how it ranks against
other labels.
What a rule can and cannot replace
- A rule fills runs that have no label, or hold an
inferred,evalor run-status-only (implicit) one. - A rule never replaces a
recordlabel, the kind you send toPOST /outcomes/by-key. If the rule disagrees with one, the rule’s result is kept as a visible, non-winning candidate (Blocked by a record label) and the run keeps its record label. If a record outcome arrives after a rule label, the record label replaces it. - A higher label (developer, classifier-confirmed, GitHub webhook) is never replaced either, and a label owned by a different webhook source is not overwritten by arrival order.
- Changing or disabling a rule creates a new version and re-evaluates against current facts. A label the old version wrote is retracted if the new version no longer supports it, and the retraction is recorded.
The templates
| Template | Passes when | Provisional success |
|---|---|---|
coding_pr_merged | The pull request is merged (a connector fact) and no verified revert merged inside the window. | Yes, from the merge until the window ends |
support_ticket_resolved | The ticket has a resolved state and no reopened state inside the window (7 days by default). | No |
Coding. Needs the GitHub connector, because the merge and revert facts
come from it. Bind it to the github_pr key type. A merged pull request passes provisionally at
the merge time and matures after the window. A verified revert inside the window fails it.
Support. Uses the generic record-state intake below, and does not imply a ticketing connection. A reopen inside the window after the resolve fails it. A reopen after the window is a later event and does not undo the success. A later resolve starts a new window. Support never passes early: it passes once the window has ended and the sender’s observation covers the whole window (see Observation).
Configuration. For each agent you set the template, the record key type the rule binds runs by,
and, for support, the source name that sends the states. You also set the mapping: which
outcome a pass writes (success) and which a fail writes (failure). With no mapping, the rule
still evaluates and stores its result, but it never writes an outcome.
Anything a rule cannot decide is unknown, never a failure: a missing fact, an unhealthy observation or an invalid definition does not write a label.
Send record state: the state field
Support rules read a typed state that rides on an ordinary
record outcome event. The state field is optional and
additive. An event without it behaves exactly as before.
curl -X POST https://api.morsehq.dev/api/v1/outcomes/by-key \
-H "Authorization: Bearer $MORSE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"key_type": "helpdesk_ticket",
"key_value": "4812",
"success": true,
"outcome": "solved",
"occurred_at": "2026-10-05T10:15:00Z",
"event_id": "ticket-4812-solved-2026-10-05T10:15:00Z",
"state": {
"source": "helpdesk",
"record_state": "resolved",
"state_occurred_at": "2026-10-05T10:15:00Z",
"source_event_id": "evt_90210",
"source_sequence": 118,
"observation": {
"complete": true,
"healthy": true,
"watermark": "2026-10-13T00:00:00Z"
}
}
}'| Field | Type | Notes |
|---|---|---|
state.source | string | Required. Names the sending system. Lower-case letters, digits and underscores, 1 to 64 characters. Must match the rule’s source. |
state.record_state | string | Required. resolved or reopened. |
state.state_occurred_at | datetime | Required. When the state changed, not when you sent it. Up to 5 minutes in the future is tolerated. |
state.source_event_id | string | Required. Your own id for this state change. A repeat of the same id for the same record is stored once. |
state.source_sequence | integer | Optional. Breaks ties between states with the same time. |
state.observation.complete | boolean | Optional, default false. You assert that you saw every change up to the watermark. |
state.observation.healthy | boolean | Optional, default false. You assert your feed was working over that span. |
state.observation.watermark | datetime | Optional. The time up to which you assert you observed this record. |
How it is used:
- The record is the event’s own
key_typeandkey_value; the state is not sent separately. - States are ordered per
sourcebystate_occurred_at, thensource_sequence, never by arrival, so a late “resolved” cannot push a newer “reopened” aside. - The state is stored even when it changes no label. Rules are evaluated after the request returns, so a rule’s label does not appear in the response.
- A malformed
stateis rejected with422before any event in the batch is applied.
Observation
A “no reopen” claim needs evidence that nothing was missed. Morse accepts a watermark only when you
send both complete: true and healthy: true, and a support rule passes only when that watermark
is at or after the end of the window.
Without a complete, healthy observation that covers the window, a support success stays provisional. Silence is not treated as proof that a ticket was not reopened.
Limitations
- A rule label is an estimate. It is never shown as human-confirmed and is counted separately from Reported outcomes.
- Merged is not code quality, and a resolved ticket is not proof the customer was satisfied.
- Morse does not predict when a rule’s label will appear. A support pass depends on your window and your observation watermark.