# RunEventType

> Tracon documentation. Tracon is a .NET package family that adds a control
> plane on Microsoft Agent Framework, published as NuGet packages that run
> inside your own ASP.NET Core application.
>
> Page: https://tracon.dev/api/tracon.runeventtype/
> Summary: RunEventType: Event types produced during a run. The user interface maps these directly onto visual elements, so the values must stay stable.

<div class="api-provenance">Namespace Tracon · Assembly <code>Tracon.Abstractions.dll</code></div>

Event types produced during a run. The user interface maps these directly onto
visual elements, so the values must stay stable.

```csharp
[JsonConverter(typeof(JsonStringEnumConverter<RunEventType>))]
public enum RunEventType
```

## Fields

`ChildRunCompleted = 9` 

A child agent run finished. `Text` carries the child agent's name and
`Payload` the child run id.

`ChildRunStarted = 8` 

This run started a child agent run. `Text` carries the child agent's
name and `Payload` the child run id.

The child run's own events are not mirrored into the root stream; only its
start and end are reported. Full mirroring would multiply the event volume
along the tree and send the same text to the client twice.

`ChildRunTimedOut = 30` 

A sub-agent run passed its wait limit. `Text` carries the child
agent's name; `Payload` is a JSON object with the child run id
(`childRunId`) and whether the cutoff was a hard one
(`hardCutoff`). When `hardCutoff` is [false](https://learn.microsoft.com/dotnet/csharp/language-reference/builtin-types/bool),
the sub-agent's own cancellation was honored and its resources were
released. When [true](https://learn.microsoft.com/dotnet/csharp/language-reference/builtin-types/bool), the sub-agent ignored
cancellation and keeps running in the background; its eventual
result is discarded without producing a further event, a metric, or
an unobserved exception.

30, not inserted earlier in the list: [RunEventType.Custom](/api/tracon.runeventtype/) is 29 and
`run_events.type` is a `smallint` column — the enum's numeric
value is what is stored. Shifting an existing member's value would
silently reinterpret every already-persisted event of that type.

`ContentBlocked = 21` 

An [IContentGuard](/api/tracon.icontentguard/) blocked content. `Text` carries the guard
name, the rule name and the direction; `Payload` carries the same four
facts as JSON (`guard`, `rule`, `direction`, `action`) —
the same shape [RunEventType.ContentMasked](/api/tracon.runeventtype/) writes.

The payload **does not carry the blocked content**. After the
event the run becomes `Failed` and `runs.error_type` is written as
`content_blocked`.

`ContentMasked = 20` 

An [IContentGuard](/api/tracon.icontentguard/) masked content. `Text` carries the guard
name, the rule name and the direction; `Payload` carries the same four
facts as JSON (`guard`, `rule`, `direction`, `action`).

Neither `Text` nor `Payload` carries the **masked
content** — they only report THAT masking happened. When the text the
model sees differs from what the user wrote, that is an event and it cannot
stay silent (that rule: a decision that cannot be recorded is a decision
that was not taken).

`Custom = 29` 

A consumer-defined event, written directly with
`AgentRunScope.Writer.AppendAsync`. `CustomType` (a
namespaced string, see [RunEventCustomTypes](/api/tracon.runeventcustomtypes/)) names it;
`Payload` carries whatever the consumer put there. Tracon
makes no claim about the payload's shape — it is the consumer's own
and out of scope for this contract. A consumer's own `Custom` event
follows the general rule in the type's remarks: with
`TraconRunRecordingOptions.RecordToolPayloads` off it still
appears in the stream but `Payload` is [null](https://learn.microsoft.com/dotnet/csharp/language-reference/keywords/null). Only
[RunEventCustomTypes.QuotaThreshold](/api/tracon.runeventcustomtypes/#Tracon_RunEventCustomTypes_QuotaThreshold) and
[RunEventCustomTypes.OutputTruncated](/api/tracon.runeventcustomtypes/#Tracon_RunEventCustomTypes_OutputTruncated) are exempt.

The escape hatch for the otherwise closed set of event types above.
When a built-in event type already fits, use that instead —
[RunEventType.Custom](/api/tracon.runeventtype/) is for events Tracon has no name for. A type
under the [RunEventCustomTypes.ReservedPrefix](/api/tracon.runeventcustomtypes/#Tracon_RunEventCustomTypes_ReservedPrefix) namespace
is rejected at write time, so a future built-in custom type can never
collide with a consumer's own.

`DocumentAttached = 24` 

A document was attached to the run through the document channel.
`Text` carries the document's name; `Payload` carries its
size in bytes and a content hash as JSON - never the document's
content itself.

`ExecutorCompleted = 15` 

An executor completed. `Text` carries the executor id.

`ExecutorFailed = 16` 

An executor failed. `Text` carries the executor id and `Payload`
a display-safe error text: a Tracon exception's own message, otherwise
`<Type> failed. (ref: <id>)`, where the id names the log
entry that holds the full exception.

A graph-level error is written with the same type; its `Text` is
the exception's type name.

`ExecutorInvoked = 14` 

An executor was invoked. `Text` carries the executor id.

`HistoryCompacted = 10` 

The chat history was compacted. `Text` carries a short summary sentence
and `Payload` the before/after message and token counts.

`LoopIterationCompleted = 31` 

One harness loop iteration was evaluated (see [LoopSettings](/api/tracon.loopsettings/)).
`Text` carries a one-line summary; `Payload` is a JSON object
with the 1-based iteration number (`iteration`), whether a criterion
asked for another iteration (`continued`), the `kind` of that
criterion (`continuedBy`, [null](https://learn.microsoft.com/dotnet/csharp/language-reference/keywords/null) when every criterion
was satisfied), whether it handed the agent feedback (`hasFeedback`),
the `kind` of a criterion that failed to evaluate
(`failedCriterion`, [null](https://learn.microsoft.com/dotnet/csharp/language-reference/keywords/null) otherwise), whether
the iteration ceiling is what ends the loop (`ceilingReached`),
whether this evaluation fails the run (`failsRun`, see
[LoopSettings.OnExhausted](/api/tracon.loopsettings/#Tracon_LoopSettings_OnExhausted) and [LoopSettings.OnCriterionError](/api/tracon.loopsettings/#Tracon_LoopSettings_OnCriterionError)),
and every criterion that was evaluated, in order (`criteria`): its `kind` and its
`decision` (`stop`, `continue` or `failed`), plus the `judge`
(`provider` and `model` an `aiJudge` really called) or the `reviewer`
(`agentName`, `agentVersion` and the child `runId` of an `agentReview`).

The event marks an **evaluated** iteration, not a model
turn. The turn that reaches [LoopSettings.MaxIterations](/api/tracon.loopsettings/#Tracon_LoopSettings_MaxIterations) is
not evaluated — there is nothing left to decide — so a loop that ends at
its ceiling writes one event fewer than it takes turns, and its last
event carries `ceilingReached`. The exception is
[LoopOutcome.FailRun](/api/tracon.loopoutcome/) on [LoopSettings.OnExhausted](/api/tracon.loopsettings/#Tracon_LoopSettings_OnExhausted):
the last turn is evaluated, the events match the turns, and an
unsatisfied last evaluation carries both `ceilingReached` and
`failsRun`.

The criterion's feedback **text** is deliberately absent:
it is free-form text written for the model, and the run event stream is
readable by the client. Its presence is reported, its content is not.

`criteria` answers "who decided". Evaluation stops at the first criterion that asks
for another iteration or fails, so the list ends there; an iteration every criterion
satisfied lists them all. A criterion that reports no judge and no reviewer carries only
its `kind` and `decision`. `reviewer.runId` is [null](https://learn.microsoft.com/dotnet/csharp/language-reference/keywords/null) when the
call was refused before a run started, and `reviewer.agentVersion` is the version that run
recorded (1 for an agent defined in code).

31, not 30: [RunEventType.ChildRunTimedOut](/api/tracon.runeventtype/) already holds 30 and
`run_events.type` is a `smallint` column, so an existing
member's numeric value can never be shifted.

`MessageCompleted = 2` 

A message completed.

`MessageDelta = 1` 

A text fragment arrived from the model. Produced only for streaming runs.

`ModelFallbackUsed = 22` 

The primary model provider was unavailable and a
[ModelBinding.Fallbacks](/api/tracon.modelbinding/#Tracon_ModelBinding_Fallbacks) link answered instead. `Text`
carries the fallback provider and model (`"{provider}/{model}"`);
`Payload` carries the primary and fallback bindings and the reason
the primary was skipped.

A model switch is never silent. This event is written in
addition to a span tag, not instead of it — an operator reading only
the run record must still see which model actually answered.

Only the agent's own binding writes it. A fallback on a binding the run
calls on the side (the compaction summarizer, an `aiJudge` judge)
does not change the run's model, so it is logged instead.

`ReasoningDelta = 23` 

A reasoning (thinking) delta arrived from the model. Off by default
(`TraconRunRecordingOptions.RecordReasoningDeltas`).

`RunAwaitingInput = 19` 

The run stopped because it waits for a human answer. It is the last event of
the stream.

Separate from [RunEventType.RunCompleted](/api/tracon.runeventtype/) and [RunEventType.RunFailed](/api/tracon.runeventtype/): the
work neither finished nor failed. On this event the user interface shows the
pending request card.

Produced for two different terminal statuses, and `Payload` differs between
them. For a workflow closing with [RunStatus.AwaitingInput](/api/tracon.runstatus/), the
details already arrived on the preceding [RunEventType.WorkflowRequest](/api/tracon.runeventtype/) event and
this one carries no payload of its own. For a root run closing with
[RunStatus.AwaitingApproval](/api/tracon.runstatus/), `Payload` is a JSON array, one
entry per pending tool call: `requestId`, `toolName`, and — when a
consumer registered an [IToolApprovalPresenter](/api/tracon.itoolapprovalpresenter/) and it resolved
something — `entityType`, `entityId`, `entityName`, and
`message` from [ToolApprovalPresentation](/api/tracon.toolapprovalpresentation/). It never carries the
call's raw arguments; those are read from [PendingApproval.Arguments](/api/tracon.pendingapproval/#Tracon_PendingApproval_Arguments)
or from the `ToolApprovalRequestContent` already present in the response.

For a root agent run closing with [RunStatus.AwaitingInput](/api/tracon.runstatus/) because an MCP server
asked the user for input in the middle of a tool call, `Payload` is a JSON array, one
entry per suspended call: `requestId`, `toolName` and `serverName`. It never
carries the requested fields or the call's arguments; the caller got them in the response.

`RunCompleted = 6` 

The run finished successfully.

`RunContinuationBlocked = 25` 

Automatic continuation of this interrupted run was refused. `Text`
carries the reason (a tool whose effect cannot be safely repeated).
Written on a run already closed [RunStatus.Failed](/api/tracon.runstatus/) by
orphaned-run reconciliation; the row itself does not change again.

`RunFailed = 7` 

The run ended with an error.

`RunStarted = 0` 

The run started.

`SessionWriteConflicted = 32` 

The run finished, but the session write that follows it lost a race and
nothing was saved to that session. `Text` carries the session id.

The run itself is [RunStatus.Completed](/api/tracon.runstatus/) and that is the
truthful status: the model was called, tokens were spent, an answer was
produced. What failed is the session write that happens AFTER the run,
and the caller was told — it received a 409.

Before this event the conflict left no trace at all in the record:
the operator saw two successful runs for the same session
and the losing side existed only in the response the caller already
consumed. When a session owner says a message went missing, the run
record now carries the evidence.

32, not 30 or 31: `run_events.type` is a `smallint` column, so
an existing member's numeric value can never be shifted.

`StructuredResponseRejected = 27` 

The response failed structured output validation and the run is
ending as [RunStatus.Failed](/api/tracon.runstatus/). `Text` carries the
safe rejection reason; `Payload` carries the same reason plus
`kind`, `schemaName`, `provider` and `model` as JSON.

Neither field carries the model's raw response text — see
[IStructuredResponseValidator](/api/tracon.istructuredresponsevalidator/).

`StructuredResponseRepairAttempted = 28` 

A bounded repair turn is about to run after a rejected structured
response. `Text` carries the attempt count as `"{attempt}/{max}"`;
`Payload` carries the same two numbers plus `kind` and
`schemaName` as JSON.

Written once per repair round, immediately before the extra model call
it describes, and only when `TraconStructuredResponseOptions.MaxRepairAttempts`
is greater than zero. See [RunEventType.StructuredResponseRejected](/api/tracon.runeventtype/), which
always precedes it: a repair round only starts after a rejection, never
on its own.

`SuperStepCompleted = 13` 

A super-step completed. `Text` carries the step number and
`Payload` the activated executor names plus the checkpoint id, if any.

`SuperStepStarted = 12` 

A super-step started. `Text` carries the step number and `Payload`
the names of the executors that sent messages.

`ToolFailed = 5` 

A tool failed.

`ToolInvoked = 4` 

A tool completed successfully. The result is in the event payload.

`ToolInvoking = 3` 

A tool is about to be called. The arguments are in the event payload.

`ToolOutputTruncated = 26` 

A tool result was trimmed to its byte limit. `ToolName` carries
the tool's name; `Text` a short summary of how many bytes were
dropped and the limit. `Payload` carries the same information as JSON.

Neither field carries the dropped content itself — the point of
trimming is to reduce volume, and writing the discarded part back
into `run_events` would undo that.

`WorkflowOutput = 17` 

The workflow produced an output. `Text` carries a text summary of it.

`WorkflowRequest = 18` 

The workflow waits for an external answer (human in the loop). `Text`
carries the request id and `Payload` a JSON summary of the request.

The payload carries *enough to rebuild* the pending request: port id,
request id, request and response type names, and the data to display.
Pending requests are read from these events; no separate table was added.
That is why this event is one of the two exempt from
`TraconRunRecordingOptions.RecordToolPayloads` (see the type's
remarks): suppressing this payload would leave the user unable to see
the question they have to answer.

`WorkflowResponseApplied = 33` 

This run continues a workflow run by answering one of its pending human
requests. `Text` carries the request id; `Payload` is
`{"sourceRunId": "...", "requestId": "..."}`, the run that waited and the
request this run answers.

Written right after [RunEventType.RunStarted](/api/tracon.runeventtype/) by every run that answers a
request, whoever started it. The answer itself is not written here: it can
hold anything the answering person typed, and the run event stream is readable
by the client. Read the decision record from
`GET /api/workflows/runs/{runId}/responses` of the source run.

The payload is written even when `RecordToolPayloads` is off: it is the
only place this run names its source.

33, not a lower free number: `run_events.type` is a `smallint`
column, so an existing member's numeric value can never be shifted.

`WorkflowStarted = 11` 

A workflow execution started. `Text` carries the workflow name.

This is separate from [RunEventType.RunStarted](/api/tracon.runeventtype/): that one reports that the
`runs` row was opened, this one reports that the Microsoft Agent
Framework execution engine actually took over the graph. Compilation and
validation happen in between, and an error there ends the run before the
graph ever starts.

## Remarks

Written to JSON **by name** (`"Code"`), not by number. The
wire contract explains itself that way and survives a change in value order.
The converter sits on the type, so the format is the same everywhere without
touching the consumer's application-wide JSON options. No enum is PERSISTED as
JSON (RunStatus and RunEventType are smallint in the database,
AgentDefinitionOrigin is rebuilt on read), so a format change does not affect
stored data.

**Every member's `Payload` is written only when
`TraconRunRecordingOptions.RecordToolPayloads` is on.**
With it off the event still appears in the stream and `Text` still
carries its summary, but `Payload` is [null](https://learn.microsoft.com/dotnet/csharp/language-reference/keywords/null) — so a
payload described below is what the field holds when it is written, not a
promise that it is always there. Two exceptions always carry theirs,
because there the payload is the function rather than an observability
detail: [RunEventType.WorkflowRequest](/api/tracon.runeventtype/), which is the only place a pending
human request can be read from, [RunEventType.WorkflowResponseApplied](/api/tracon.runeventtype/), which
is the only link from an answer's run back to the run it answered, and
[RunEventType.Custom](/api/tracon.runeventtype/) of the two notice types `RunEventCustomTypes.QuotaThreshold`
and `RunEventCustomTypes.OutputTruncated`, whose payload carries the dedup
key a client needs to act on the frame. A consumer's own [RunEventType.Custom](/api/tracon.runeventtype/)
event is not exempt, and neither are the other reserved types
(`RunEventCustomTypes.Content`, `RunEventCustomTypes.WorkflowEvent`).
