The Best Agent Architectures Are Event-Driven, Not Chat-Driven
Chat is one trigger among many. Design for events first, and conversation becomes a natural special case.

Most agent tutorials start the same way: a user sends a message, the agent responds. Chat in, chat out. It's the default mental model, and it's limiting what teams build.
The agents that do the most useful work in production aren't waiting for someone to type. They're reacting to events — a webhook fires, a document lands in a queue, a timer ticks, a row changes in a database. The conversation interface is one trigger among many, not the only one.
If your agent architecture assumes chat, you'll keep bolting on workarounds. If it assumes events, chat becomes a natural special case.
The Chat Trap
When you model your agent around a conversation loop, every interaction gets squeezed through the same pipe: user sends message → agent processes → agent responds.
This works fine for support bots and coding assistants. But the moment you need an agent to:
- Process an incoming webhook from Stripe when a payment fails
- Analyze a document the moment it's uploaded to S3
- Run a nightly reconciliation across two data sources
- React to a monitoring alert and create a ticket
...you're fighting the architecture. You start wrapping your chat-based agent in cron jobs, queue consumers, and API routes that fake a "user message" to trigger the behavior you want. The agent becomes a chat interface with duct tape on the edges.
Events Are the Better Primitive
An event-driven agent architecture starts from a different assumption: the agent is a thing that responds to triggers, and a user message is just one type of trigger.
This isn't a hypothetical. It's how Octavus protocols work. Every agent declares its triggers explicitly:
triggers:
user-message:
description: User sends a chat message
input:
USER_MESSAGE:
type: string
payment-failed:
description: Stripe webhook for failed payment
input:
CUSTOMER_ID:
type: string
INVOICE_ID:
type: string
FAILURE_REASON:
type: string
document-uploaded:
description: New document ready for processing
input:
DOCUMENT_URL:
type: string
ANALYSIS_TYPE:
type: string
Each trigger has a name, a typed input schema, and a corresponding handler that defines what the agent does when that trigger fires. The user-message trigger is just another entry in the list — no more special than payment-failed or document-uploaded.
Handlers: Different Events, Different Behavior
Each trigger maps to its own handler. This is where event-driven design pays off — you're not routing everything through a single prompt and hoping the model figures out context from the message text.
handlers:
user-message:
Add user message:
block: add-message
role: user
prompt: user-message
input: [USER_MESSAGE]
Respond:
block: next-message
payment-failed:
Look up customer:
block: tool-call
tool: get-customer
input:
customerId: CUSTOMER_ID
Assess failure:
block: add-message
role: user
prompt: payment-failure-assessment
input: [CUSTOMER_ID, INVOICE_ID, FAILURE_REASON]
Decide action:
block: next-message
document-uploaded:
Analyze document:
block: add-message
role: user
prompt: document-analysis
input: [DOCUMENT_URL, ANALYSIS_TYPE]
Generate analysis:
block: next-message
The payment-failed handler doesn't start with a chat message. It starts with a deterministic tool call to look up customer data, then feeds structured context to the model. The document-uploaded handler goes straight to analysis. Each path is designed for its event, not shoehorned through a conversational flow.
Firing Triggers from Anywhere
Once your agent speaks events, invoking it from any part of your stack becomes straightforward. The server SDK's execute() method takes a trigger name and typed input — that's it:
// From a webhook endpoint
app.post('/webhooks/stripe', async (req, res) => {
const event = req.body;
if (event.type === 'invoice.payment_failed') {
const session = client.agentSessions.attach(sessionId, {
tools: { 'get-customer': getCustomerHandler },
});
const events = session.execute({
type: 'trigger',
triggerName: 'payment-failed',
input: {
CUSTOMER_ID: event.data.object.customer,
INVOICE_ID: event.data.object.id,
FAILURE_REASON: event.data.object.last_payment_error?.message,
},
});
for await (const ev of events) {
// Process streaming events
}
}
});
// From a scheduled job
cron.schedule('0 2 * * *', async () => {
const sessionId = await client.agentSessions.create('reconciliation-agent');
const session = client.agentSessions.attach(sessionId, {
tools: { 'query-database': queryHandler },
});
const events = session.execute({
type: 'trigger',
triggerName: 'run-reconciliation',
input: { DATE: new Date().toISOString().split('T')[0] },
});
for await (const ev of events) {
// Handle results
}
});
No fake user messages. No prompt injection to set context. The trigger carries the event type and the data it needs. The handler knows what to do with it.
Workers for Pure Event Processing
Some events don't need a conversation at all. They need execution: take input, run steps, produce output. That's what workers are for.
Workers are a separate agent format in Octavus — no triggers, no session persistence, just sequential steps with a return value:
input:
DOCUMENT_URL:
type: string
ANALYSIS_TYPE:
type: string
variables:
EXTRACTED_DATA:
type: string
ANALYSIS_RESULT:
type: string
steps:
Start extraction:
block: start-thread
thread: extraction
model: anthropic/claude-sonnet-4-5
system: extraction-system
tools: [fetch-document]
maxSteps: 3
Request extraction:
block: add-message
thread: extraction
role: user
prompt: extract-prompt
input: [DOCUMENT_URL]
Extract:
block: next-message
thread: extraction
output: EXTRACTED_DATA
Start analysis:
block: start-thread
thread: analysis
model: anthropic/claude-sonnet-4-5
system: analysis-system
Request analysis:
block: add-message
thread: analysis
role: user
prompt: analyze-prompt
input: [EXTRACTED_DATA, ANALYSIS_TYPE]
Analyze:
block: next-message
thread: analysis
output: ANALYSIS_RESULT
output: ANALYSIS_RESULT
Workers can use different models at different steps, run tools agentically, and return a structured output. Call them from a queue consumer, a webhook handler, or from inside another agent:
const { output } = await client.workers.generate(
'document-analyzer',
{ DOCUMENT_URL: url, ANALYSIS_TYPE: 'summary' },
{
tools: {
'fetch-document': async (args) => await fetchDoc(args.url),
},
},
);
No session. No conversation history. Just input → processing → output. The event-driven agent for cases where statefulness is unnecessary weight.
Sessions Still Matter — For the Right Events
This isn't an argument against sessions or conversation. Stateful sessions are powerful when an agent needs to maintain context across a series of related events. A support agent handling an ongoing customer issue, for example, benefits from remembering what happened three messages ago.
The point is that sessions serve the event architecture, not the other way around. You create a session when an event stream needs continuity. You skip it when an event is self-contained.
// Self-contained event → Worker (no session)
const result = await client.workers.generate('analyze-doc', { DOCUMENT_URL: url });
// Ongoing event stream → Session (stateful)
const sessionId = await client.agentSessions.create('support-agent', { USER_ID: userId });
// Multiple triggers over time share this session's state
The Design Shift
Moving from chat-driven to event-driven agent architecture changes how you think about several things:
Agent boundaries. Instead of one mega-agent that handles everything through conversation, you design agents around the events they respond to. An agent that handles payment failures is separate from one that analyzes documents. Clearer boundaries, easier testing, better separation of concerns.
Input contracts. Every trigger has a typed input schema. When a webhook fires, the data it carries is validated against that schema. No more parsing intent from natural language when you already have structured data.
Composition. Workers can be called from other agents. An interactive agent handling a customer conversation can delegate to a worker for document analysis mid-conversation, then continue the dialogue with the results. Events compose naturally because they have defined inputs and outputs.
Observability. When every invocation is a named trigger with typed input, logging and debugging get dramatically simpler. You can trace exactly which event fired, what data it carried, and which handler processed it. Compare that to debugging a chat log to figure out what prompt led to what behavior.
Start with Events
If you're designing a new agent system, resist the urge to start with a chat interface and add event handling later. Start with triggers. Define the events your agent needs to respond to. Chat will be one of them — probably the one you build a UI for — but it won't constrain your architecture.
The agents that do the most work in production are the ones you never talk to.



