Skip to Content
IntegrationsOutcome rules

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, eval or run-status-only (implicit) one.
  • A rule never replaces a record label, the kind you send to POST /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

TemplatePasses whenProvisional success
coding_pr_mergedThe 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_resolvedThe 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" } } }'
FieldTypeNotes
state.sourcestringRequired. Names the sending system. Lower-case letters, digits and underscores, 1 to 64 characters. Must match the rule’s source.
state.record_statestringRequired. resolved or reopened.
state.state_occurred_atdatetimeRequired. When the state changed, not when you sent it. Up to 5 minutes in the future is tolerated.
state.source_event_idstringRequired. Your own id for this state change. A repeat of the same id for the same record is stored once.
state.source_sequenceintegerOptional. Breaks ties between states with the same time.
state.observation.completebooleanOptional, default false. You assert that you saw every change up to the watermark.
state.observation.healthybooleanOptional, default false. You assert your feed was working over that span.
state.observation.watermarkdatetimeOptional. The time up to which you assert you observed this record.

How it is used:

  • The record is the event’s own key_type and key_value; the state is not sent separately.
  • States are ordered per source by state_occurred_at, then source_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 state is rejected with 422 before 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.
Last updated on