Skip to content
← all posts
10 min read

The Complete Guide to AI Agent Events

Mentiko Team

Events are how agents talk to each other in Mentiko. Not through shared memory, not through function calls, not through message queues. Through files. An agent finishes its work, writes a file, and any agent watching for that file wakes up and starts. That's the entire coordination model.

This post is the complete reference for how events work: the file format, the trigger/emit wiring, conditional routing, fan-out and fan-in, error handling, and debugging. Bookmark this one.

What events actually are

An event is a file that an agent writes to the .events/ directory when it completes. The event system watches that directory. When a new file appears, the system reads it, determines which agents are listening for that event type, and starts them.

Events are the connective tissue between agents in a chain. Agent A doesn't call Agent B. Agent A writes a file. Agent B happens to be watching for that file. They don't know about each other. They share a file format, not an interface.

This has a practical consequence that matters more than any architectural principle: events are files you can cat, grep, and git diff. There's no hidden state. No internal message bus you need special tooling to inspect. The entire coordination history of a chain run is a directory listing.

$ ls .events/
researcher-complete.event
writer-complete.event
editor-complete.event

$ cat .events/researcher-complete.event
{
  "event": "research:complete",
  "status": "success",
  "output": ".outputs/researcher/findings.md",
  "tokens_used": 14230,
  "timestamp": "2026-03-19T10:15:42Z"
}

Event file format

The event system is intentionally forgiving about format. It accepts JSON, YAML, or markdown frontmatter. The parser tries them in that order, and if all three fail, the filename itself becomes the event identifier.

JSON is the most common:

{
  "event": "research:complete",
  "status": "success",
  "output": ".outputs/researcher/findings.md"
}

YAML works if you prefer it:

event: research:complete
status: success
output: .outputs/researcher/findings.md

Markdown frontmatter works too, which is useful when an agent's output is itself a markdown document:

---
event: research:complete
status: success
output: .outputs/researcher/findings.md
---

Research completed. Found 12 relevant sources across 3 databases.

The only required field is event -- the event name. Everything else is metadata that downstream agents can read but the system doesn't enforce. In practice, you'll almost always include status (success or error) and output (path to the agent's actual output file).

Why the format flexibility? Because agents write these files. An agent prompted to "write your results as JSON" will produce JSON. An agent prompted to "write a research report" might produce markdown. Rather than force every agent to serialize a specific schema, the event system meets agents where they are.

Trigger and emit patterns

Every agent in a chain definition declares two things: what events it listens for (triggers) and what events it produces (emits).

{
  "name": "writer",
  "triggers": ["research:complete"],
  "emits": ["draft:complete"]
}

This agent starts when it sees a research:complete event and produces a draft:complete event when it finishes. The trigger/emit declarations are how you wire agents together without coupling them directly.

Two special event names are reserved: chain:start fires when the chain is first invoked (it's how the first agent knows to begin), and chain:complete signals that the chain is done (it's how the system knows no more agents will run).

Here's a complete three-agent chain showing the wiring:

{
  "name": "content-pipeline",
  "agents": [
    {
      "name": "researcher",
      "prompt": "Research {TOPIC} and compile findings into a structured brief.",
      "triggers": ["chain:start"],
      "emits": ["research:complete"]
    },
    {
      "name": "writer",
      "prompt": "Write a 1500-word article from the research brief.",
      "triggers": ["research:complete"],
      "emits": ["draft:complete"]
    },
    {
      "name": "editor",
      "prompt": "Edit the draft for clarity, accuracy, and tone.",
      "triggers": ["draft:complete"],
      "emits": ["chain:complete"]
    }
  ]
}

The chain flows: chain:start -> researcher -> research:complete -> writer -> draft:complete -> editor -> chain:complete. Each arrow is a file being written and watched.

An agent can have multiple triggers. A writer that accepts both fresh research and revision feedback would declare "triggers": ["research:complete", "review:revision-needed"]. It starts whenever either event appears. This is how you build loops and merge points without modifying the upstream agents.

Conditional events

Agents aren't limited to a single outcome. An agent can emit different events depending on what it found, and downstream agents can trigger on specific outcomes. This is how you build branching logic without drawing complex graphs.

Consider a quality gate agent that reviews a draft and either approves it or sends it back:

{
  "name": "quality-gate",
  "prompt": "Review the draft against our style guide. If it passes all checks, emit review:pass. If it fails any check, emit review:fail with the specific failures listed.",
  "triggers": ["draft:complete"],
  "emits": ["review:pass", "review:fail"]
}

The emits array lists all possible events this agent can produce. At runtime, it only emits one (or sometimes a subset). Downstream agents wire to the specific outcome they care about:

{
  "name": "publisher",
  "triggers": ["review:pass"],
  "emits": ["chain:complete"]
},
{
  "name": "revision-agent",
  "triggers": ["review:fail"],
  "emits": ["draft:complete"]
}

The publisher only starts if the review passed. The revision agent only starts if it failed -- and notice that the revision agent emits draft:complete, which sends the revised draft back through the quality gate. You've built a review loop with three independent agent definitions and zero control flow logic.

The event content carries the reasoning. When the quality gate emits review:fail, the event file includes what failed:

{
  "event": "review:fail",
  "status": "failed",
  "failures": ["intro_too_long", "missing_code_examples", "passive_voice"],
  "output": ".outputs/quality-gate/review.md"
}

The revision agent reads this and knows exactly what to fix. The information flows through files, not function parameters.

Fan-out and fan-in

Fan-out happens when one event triggers multiple agents. Fan-in happens when one agent waits for multiple events before starting. Combined, they give you parallelism.

Fan-out is implicit: multiple agents declare the same trigger, and they all start when that event appears.

{
  "name": "academic-searcher",
  "triggers": ["query:ready"],
  "emits": ["search:complete"]
},
{
  "name": "news-searcher",
  "triggers": ["query:ready"],
  "emits": ["search:complete"]
},
{
  "name": "patent-searcher",
  "triggers": ["query:ready"],
  "emits": ["search:complete"]
}

When query:ready fires, all three agents start simultaneously. They run in parallel on separate threads, each doing their own search.

Fan-in uses the collect field to wait for a specific number of events before starting:

{
  "name": "synthesizer",
  "triggers": ["search:complete"],
  "collect": 3,
  "emits": ["chain:complete"]
}

The synthesizer watches for search:complete events. After it has collected three of them (one from each searcher), it starts. It reads all three event files and their associated output files to produce a unified result.

Why an explicit count instead of "wait for all"? Because "all" is ambiguous at runtime. If one searcher crashes silently, an unbounded wait hangs the entire chain. A fixed count with a timeout gives you a definitive failure signal: "expected 3 search:complete events, got 2 after 120 seconds, chain stalled."

You can also pair fan-in with a timeout to produce partial results:

{
  "name": "synthesizer",
  "triggers": ["search:complete"],
  "collect": 3,
  "timeout": 60,
  "on_timeout": "proceed_with_available",
  "emits": ["chain:complete"]
}

If only two searchers finish within 60 seconds, the synthesizer proceeds with what it has. This is the right default for most production chains -- partial results beat no results.

Error events

When an agent fails, the system automatically emits an error event. You don't need to configure this. Every agent has an implicit error path.

The error event includes the agent name, error type, error message, and a truncated stack trace:

{
  "event": "agent:error",
  "agent": "academic-searcher",
  "error_type": "context_length_exceeded",
  "error_message": "Input tokens (142000) exceed model limit (128000)",
  "stack_trace": "...",
  "timestamp": "2026-03-19T10:18:33Z",
  "input_event": "query:ready"
}

You can override the automatic error event name with on_error:

{
  "name": "primary-api",
  "triggers": ["chain:start"],
  "emits": ["data:ready"],
  "on_error": "primary:failed"
}

Now you can wire a fallback agent specifically to this agent's failures:

{
  "name": "fallback-api",
  "triggers": ["primary:failed"],
  "emits": ["data:ready"]
}

The downstream agent that consumes data:ready doesn't know or care which API agent produced it. It gets the same event type either way. This is the same loose coupling that makes the whole system work -- but applied to error recovery.

For broader error handling, you can create a recovery router that watches all error events:

name: recovery-router
watch: "agent:error"
prompt: |
  An agent failed. Read the error event and decide how to recover:
  - context_length_exceeded: route to summarizer, then retry
  - rate_limited: schedule retry with exponential backoff
  - api_403: route to alternative source agent
  - unknown: alert the operator and halt the chain

The recovery router is itself just an agent. It reads the error event, decides what to do, and emits the appropriate next event. Your error handling is as sophisticated as an LLM can be, not limited to retry-with-backoff.

Debugging events

Events are files. Debugging events is debugging files. Every tool you already know works.

List all events from a chain run:

$ ls -lt .events/
-rw-r--r--  1 user  staff  234 Mar 19 10:18 editor-complete.event
-rw-r--r--  1 user  staff  198 Mar 19 10:17 writer-complete.event
-rw-r--r--  1 user  staff  312 Mar 19 10:15 researcher-complete.event

The timestamps tell you the execution timeline. The file sizes hint at how much metadata each agent wrote.

Read a specific event:

$ cat .events/writer-complete.event
{
  "event": "draft:complete",
  "status": "success",
  "output": ".outputs/writer/draft.md",
  "tokens_used": 28450,
  "model": "claude-sonnet-4-20250514",
  "timestamp": "2026-03-19T10:17:22Z"
}

Find failures across all events:

$ grep "status" .events/*.event
.events/researcher-complete.event:  "status": "success",
.events/writer-complete.event:      "status": "success",
.events/editor-complete.event:      "status": "error",

Compare runs:

$ diff .events-previous/ .events/
< "tokens_used": 14230
> "tokens_used": 31200

Track changes over time:

$ git diff .events/

If your events are committed (and they should be for reproducible chains), git log .events/ gives you a complete history of every chain run, what changed, and when.

The Mentiko watchdog also monitors for stalled events. If no new event file appears after a configurable timeout (default: 300 seconds), the watchdog emits a chain:stalled event with diagnostic information: which agent was expected to run, what event it was waiting for, and the last event that was successfully written. You can wire an alerting agent to chain:stalled that sends a notification to Slack, PagerDuty, or wherever you want.

Best practices

These are the conventions we've landed on after running thousands of chains in production. None of them are enforced by the system. All of them will save you debugging time.

Name events descriptively. Use research:complete not done. Use review:fail not bad. Event names are the only coordination contract between agents -- make them self-documenting. The namespace:action convention (colon-separated) scales well.

Include metadata in event content. Timestamps, token counts, model names, output paths. You'll need this information when debugging and you won't want to reconstruct it from logs. The event file is cheap storage for expensive-to-recompute context.

Use namespaced events for complex chains. When a chain has multiple phases, prefix events with the phase: phase1:research:complete, phase2:writing:draft-ready. This prevents collisions when two agents in different phases would otherwise emit identically-named events.

Keep event files small. An event is metadata about work, not the work itself. The event should contain a path to the output ("output": ".outputs/researcher/findings.md"), not the output itself. This keeps event parsing fast and event directories manageable. Large event files slow down the watcher and bloat your git history.

Let agents write output to files, events point to the output. The agent's actual work product goes in .outputs/agent-name/. The event file goes in .events/. This separation means you can rm -rf .events/ and re-run a chain without losing any work product, and you can inspect work products without wading through event metadata.

Validate event schemas at deploy time. Before a chain runs, Mentiko checks that every trigger declared on an agent matches at least one emit declared on another agent (or is a special event like chain:start). This catches wiring mistakes -- an agent watching for reserach:complete (typo) when the emitter writes research:complete -- before they cause silent failures at runtime.

Commit your event schemas, not your event files. The chain definition (which declares triggers and emits) belongs in version control. The actual event files from a run are ephemeral -- keep them for debugging, archive them for audit trails, but don't clutter your repo with per-run artifacts.

What's next

Events are the foundation that everything else in Mentiko builds on. Chains are wired through events. Scheduling triggers events on a cron. The visual builder is a UI for declaring triggers and emits. The monitoring dashboard watches the .events/ directory in real time.

If you're just getting started, read Your First Agent Chain in Five Minutes for a hands-on tutorial, or 5 Agent Chain Patterns to see how triggers and emits compose into real-world architectures.

Ready to build? Join the waitlist and we'll set up your instance.

Get new posts in your inbox

No spam. Unsubscribe anytime.