Choosing an executor for external calls
Three step types can all make a step do something outside the pipeline
itself: executor: gateway (an LLM, optionally with MCP tools), executor: webhook (an HTTP request), and executor: shell (a local script). All
three are, in the narrow sense, “capable” of calling a REST API — nothing
stops a shell script from opening a socket. Capability isn’t the question
that decides between them. Trust model and response shape are.
Start with: does this step need to reason, or just fetch?
Section titled “Start with: does this step need to reason, or just fetch?”If the step’s job is to interpret something — decide what an alert means,
draft a message, judge whether data looks anomalous — you need an LLM:
executor: gateway. Give the agent an MCP tool grant (tools:) if it needs
to call something itself as part of that reasoning; see
Executors.
If the step’s job is purely mechanical — fetch a record, POST a status update, run a threshold check — an LLM call is the wrong tool: slower, more expensive, and non-deterministic for a task that has exactly one right answer. That’s the rest of this page.
Between webhook and shell: it’s a trust question, not a capability one
Section titled “Between webhook and shell: it’s a trust question, not a capability one”executor: webhook — a pipeline author declares a URL, method, headers,
and body inline in pipeline YAML. No operator approval needed beyond
whatever already lets a pipeline exist at all. parse_json: true parses the
response as JSON and exposes its fields to later steps — see
Executors — webhook
and the tutorial that builds one.
executor: shell — a pipeline can only reference a script by name;
an operator must have separately added that exact, checksum-pinned
script to security.shell_steps.allowed in config.yaml before any
pipeline can use it at all. See
Executors — shell
and the tutorial that builds one.
Nothing prevents a shell script from making an HTTP call too — there’s no
network sandboxing, only CPU/memory/filesize limits and an empty
environment (see that tutorial for the gotcha that causes). So in the
narrowest sense, shell can do everything webhook can and more. That’s
exactly why it’s the wrong default for “call an API”: its trust model
requires an operator to pre-vet and pin one specific script before a
pipeline author can use it for anything, which is the right bar for “run
arbitrary local code” and the wrong bar for “call this URL, I need its JSON
fields in my next step” — something that should be as easy to declare as
the url:/headers: a pipeline author can already set on any webhook
step, with no operator round-trip every time an API’s shape changes or a
new endpoint needs calling.
Reach for shell instead when the step genuinely needs local logic an
HTTP round-trip can’t express — a deterministic transform, a call to an
internal CLI tool, something with no clean single URL. And reach for it
when your deployment’s security posture specifically wants every outbound
call pinned and reviewed, not just declared — shell’s operator-approval
model is strictly tighter than webhook’s “any pipeline author can point
this at any URL,” which is a real, deliberate difference worth choosing on
purpose rather than defaulting past.
Quick reference
Section titled “Quick reference”| Need | Executor |
|---|---|
| Interpret, judge, draft prose | gateway |
| Call an API, use its JSON response in a later step | webhook + parse_json: true |
| Fire a notification, don’t need the response | webhook (parse_json omitted) |
| Local deterministic logic (transform, threshold check, internal CLI tool) | shell |
| Any outbound call must be operator-vetted and pinned, not just author-declared | shell |
Where next
Section titled “Where next”- Executors — the full adapter pattern and
every concrete executor’s
executor_configschema. - Tutorial: call an API and use its response
- Tutorial: run deterministic logic as a pipeline step
- Samples — runnable examples of both.