# `Nous.Workflow.Telemetry`
[🔗](https://github.com/nyo16/nous/blob/v0.17.1/lib/nous/workflow/telemetry.ex#L1)

Telemetry events for workflow execution.

## Workflow Events

  * `[:nous, :workflow, :run, :start]` — workflow begins
    * Measurement: `%{system_time: integer}`
    * Metadata: `%{workflow_id: string, workflow_name: string, node_count: integer}`

  * `[:nous, :workflow, :run, :stop]` — workflow completes
    * Measurement: `%{duration: integer}`
    * Metadata: `%{workflow_id: string, status: atom, nodes_executed: integer}`

  * `[:nous, :workflow, :run, :exception]` — workflow fails
    * Measurement: `%{duration: integer}`
    * Metadata: `%{workflow_id: string, reason: term}`

## Node Events

  * `[:nous, :workflow, :node, :start]` — node begins
    * Measurement: `%{system_time: integer}`
    * Metadata: `%{workflow_id: string, node_id: string, node_type: atom}`

  * `[:nous, :workflow, :node, :stop]` — node completes
    * Measurement: `%{duration: integer}`
    * Metadata: `%{workflow_id: string, node_id: string, node_type: atom, success: boolean}`

  * `[:nous, :workflow, :node, :exception]` — node fails
    * Measurement: `%{duration: integer}`
    * Metadata: `%{workflow_id: string, node_id: string, node_type: atom, reason: term}`

## Consuming the events

Attach a `:telemetry` handler once at boot; nothing in this module needs to
be called from application code — `Nous.Workflow.Engine` emits every event
above as it runs a graph.

    :telemetry.attach_many(
      "my-app-workflow-metrics",
      [
        [:nous, :workflow, :run, :stop],
        [:nous, :workflow, :node, :stop]
      ],
      &MyApp.Metrics.handle_workflow_event/4,
      nil
    )

## Emitting the events

The functions below are the emitters `Nous.Workflow.Engine` calls. They are
public so that a custom engine can produce the same event stream (and so the
payloads above have a single source of truth), but a workflow built with
`Nous.Workflow` never needs to call them directly.

Every `start_time` argument is a `System.monotonic_time/0` reading taken when
the corresponding `:start` event was emitted — the `:stop` and `:exception`
events turn it into the `:duration` measurement.

# `start_time`

```elixir
@type start_time() :: integer()
```

Monotonic timestamp captured when the matching `:start` event fired.

# `node_exception`

```elixir
@spec node_exception(String.t(), String.t(), atom(), start_time(), term()) :: :ok
```

Emit `[:nous, :workflow, :node, :exception]`.

`reason` is the raised error or exit reason, unwrapped — attach a handler to
this event to report node crashes without instrumenting each node.

# `node_start`

```elixir
@spec node_start(String.t(), String.t(), atom()) :: :ok
```

Emit `[:nous, :workflow, :node, :start]`.

`node_type` is the `t:Nous.Workflow.Node.node_type/0` of the node about to
run, which is what makes per-node-kind latency breakdowns possible.

# `node_stop`

```elixir
@spec node_stop(String.t(), String.t(), atom(), start_time(), boolean()) :: :ok
```

Emit `[:nous, :workflow, :node, :stop]`.

A node that returned an error still stops normally — `success` is `false`
and no `:exception` event is emitted. Reserve `node_exception/5` for nodes
that raised or crashed.

# `workflow_exception`

```elixir
@spec workflow_exception(String.t(), start_time(), term()) :: :ok
```

Emit `[:nous, :workflow, :run, :exception]`.

Emitted instead of `workflow_stop/4` when the run aborts, so a handler that
only watches `:stop` will not double-count a failed run.

# `workflow_start`

```elixir
@spec workflow_start(String.t(), String.t() | nil, non_neg_integer()) :: :ok
```

Emit `[:nous, :workflow, :run, :start]`.

Measurements carry both `:system_time` (wall clock, for correlating with
logs) and `:monotonic_time` — pass the latter back as `start_time` to
`workflow_stop/4` or `workflow_exception/3`.

## Examples

    iex> :telemetry.attach(
    ...>   "doctest-workflow-start",
    ...>   [:nous, :workflow, :run, :start],
    ...>   fn _event, _measurements, metadata, pid -> send(pid, {:telemetry, metadata}) end,
    ...>   self()
    ...> )
    iex> Telemetry.workflow_start("wf_1", "research", 3)
    iex> :telemetry.detach("doctest-workflow-start")
    iex> receive do: ({:telemetry, metadata} -> metadata)
    %{workflow_id: "wf_1", workflow_name: "research", node_count: 3}

# `workflow_stop`

```elixir
@spec workflow_stop(String.t(), start_time(), atom(), non_neg_integer()) :: :ok
```

Emit `[:nous, :workflow, :run, :stop]`.

`status` is the engine's terminal status for the run (`:completed`,
`:failed`, `:cancelled`, …) and `nodes_executed` counts the nodes that
actually ran, not the nodes in the graph.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
