What an Agent Can Actually Do (And When It Does It)
"What can your agent do?" is two questions wearing one coat. The first is about the action space: once the agent is awake and thinking, what can it reach, call, read, and change? The second is about the trigger space: what makes it wake up in the first place? Teams conflate them constantly, and the conflation is expensive, because the two want opposite answers.
We build for a deliberate asymmetry. The action space is as wide as we can responsibly make it — a real Python kernel, a shell, roughly a hundred curated third-party integrations, and permission to write loops instead of taking turns. The trigger space is three entries long. Not three for now, pending a roadmap. Three because every entry point into an autonomous system is a surface someone has to reason about, and we would rather have three surfaces we understand completely than thirty we understand approximately.
Three ways in, enforced in the schema
Here is the entire trigger space, as a StrEnum backed by an eight-character
database column:
class TriggerType(StrEnum):
CHAT = "chat"
RUN = "run"
SCHEDULE = "schedule"
That column width is not an accident of history. Eight characters is a wall. Adding a fourth trigger means a migration, which means a conversation, which is exactly the friction we want on this particular axis. Cheap to add is the wrong property for an entry point into a system that takes real actions.
| Trigger | Surface | What it means |
|---|---|---|
chat | studio_chat | Interactive conversation with the agent |
run | studio_run | A person clicks "run now" |
schedule | scheduled_job | Cron fires |
That's the list. There is no webhook trigger. No email-arrival trigger. No "someone posted in a channel" trigger. We know those are the first three features on most people's wish list, and we know exactly what the absence looks like from outside: a gap.
We'd argue it's a position.
Why a narrow trigger set is the feature
The argument against webhooks isn't that they're hard. They're easy — that's the problem. Every webhook you accept is an entry point where the outside world decides when your agent runs, how often, and with what payload. Each one needs its own authentication story, its own replay semantics, its own idempotency reasoning, its own answer to "what happens when this fires four hundred times in a minute because an upstream system had a bad afternoon." That reasoning doesn't amortize across endpoints. It's per-endpoint, forever.
A schedule is a trigger you can reason about. It fires when you said it would. Its concurrency is bounded by its own period. Its payload is whatever the agent goes and fetches. And critically — you can poll from a schedule. Nearly every "I need a webhook" requirement is really "I need to know about this within N minutes," and a schedule answers that with a mechanism whose failure modes fit in your head. You trade a little latency for an entry point that cannot be triggered by anyone outside your system. That trade is available to you at any time, and it's usually the right one.
The discipline shows up elsewhere too. There is an internal event job type in
the platform, and it would have been the natural place to sneak a fourth
trigger in. It doesn't start agent runs. It handles post-run work —
consolidating memory, scoring KPIs. Work that happens after an execution, on
the same rails, never as a way in. The trigger set stayed three because we kept
it three when it was inconvenient.
One path, many entry points
Here's the part that makes the narrow set work rather than merely sound principled.
A scheduled run is not a special execution mode. It is not batch mode. It is not
a lighter-weight cousin of the interactive path with a few features stubbed out.
It goes down the identical orchestration path as a user-initiated run —
different surface, different trigger_type, same everything else. The intent is
written into the module itself: keep the scheduled run "on the identical
orchestration path as a user-initiated run."
You can see the discipline hold under pressure at the one place you'd expect it
to bend. When someone manually triggers a schedule — the "run this now, don't
wait for 6am" button — the obvious implementation is a shortcut: skip the
schedule machinery, fire a direct run. We don't. It creates an execution with
trigger_source="manual" and dispatches immediately, leaving the recurring
schedule intact, and it reaches the agent through the same schedule path. The
manual trigger is a nudge to an existing mechanism, not a bypass around it.
The alternative is where divergence bugs live. A separate batch mode starts as ninety percent shared code. Then the scheduled path gets a fix the interactive one doesn't. Then someone adds a guard to the interactive path because that's where the user complaints come from. Six months later you have two systems with one name, and the failure you're debugging only reproduces at 4am on Tuesdays. One path with many entry points means a fix lands once, and every trigger inherits it. It also means the trigger set can stay small without costing capability — because narrowing the ways in doesn't narrow what happens once you're in.
Now the wide half
Everything above is about restraint. None of it applies to the action space, where the answer is: as much as we can hand it.
The agent has a real computer. Inside the box, a set of tools run locally —
run_python, bash, text_editor, query_file, read_file, update_plan,
load_tools, get_tool_details, fetch_full_result, query_resource,
user_input, task_complete, save_memory. These aren't metered as external
operations, because they aren't external. They're the agent using its own
machine.
Above that sit first-party servers: web search, the Brain knowledge and search layer, a platform "system" server, and a tool catalog the agent queries to find out what else exists. And beyond those, roughly a hundred curated third-party integrations reached through our integration layer — CRM and sales, communication, finance, documents and storage, calendar and meetings. Wide by category, curated rather than infinite, but wide.
Code-mode: the part most readers don't expect
The default mental model of an agent is a turn-taker. Think, call one tool, read the result, think again. That model is a ceiling, and most of it is self-imposed.
The preamble teaches a three-tier ladder instead:
1. Answer directly — no tool needed.
2. Call a tool directly — for ONE discrete action.
Don't wrap a one-shot call in code.
3. Write and run code — for orchestration: chaining calls,
looping, fetching in bulk.
The rung that matters is the third, and the guidance that makes it click is blunt: "Calling the same tool across a list is a LOOP — write it once in code." An agent asked to touch forty records shouldn't take forty turns. It should write the loop. The Python kernel persists across cells, so state carries between steps — fetch in one, transform in the next, act in a third, without rebuilding the world each time.
Tier two matters as much, in the other direction. The failure mode of a capable agent is wrapping a single API call in eleven lines of Python because code feels more thorough. "Don't wrap a one-shot call in code" is the counterweight. The ladder is a ladder — you're supposed to stop climbing when you've reached the rung that fits.
Also worth knowing: large tool results never enter the conversation. The agent gets a short preview plus a file reference it queries with SQL. So "what it can do" includes working over data far larger than a context window — the window holds the pointer, not the payload.
The asymmetry, stated plainly
Widen what an agent can do once it's running. Narrow the number of ways it can start running. Those pull in opposite directions and they should — they're protecting different things.
The action space is bounded by the agent's judgment inside a run you initiated, observed under permissions you set. Getting it wrong costs you one bad run. The trigger space is bounded by nothing except the entry points you built, and every one you add is a door someone else's system can knock on at a time you didn't choose. Getting that wrong costs you the property that made the whole thing governable.
So: three triggers, one path, a real computer, and permission to write a loop. If you want an agent to react to something we don't trigger on, poll for it from a schedule. It's less elegant. It's a mechanism you can hold in your head, and after enough production systems, that stops feeling like a consolation prize.
Written by CatalEx Engineering. We build the AI operating layer for AI-native companies — one platform to build, deploy, and run AI agents in production. More at catalex.co.