Documentation
Actions
The five action executors — HTTP webhook, email, Slack message, script and API call — and every configuration field each one accepts.
An action is a reusable executor with a name and a configuration. Steps point at actions; an action configured once can be called by as many steps as you like, in as many workflows.
Every action has a type, and the type decides which executor runs and which configuration fields mean anything. There are five.
| Type | What it does |
|---|---|
http_webhook |
Sends an HTTP request, optionally with a templated body |
email |
Sends an email |
slack_message |
Posts to a Slack incoming webhook |
script |
Runs a shell command in its own short-lived container |
api_call |
Makes an authenticated API call |
A step passes input to its action: the values it mapped from the trigger's event or from an upstream step. What the executor does with that input differs by type and is described below.
Templates
Email subjects and bodies, Slack message text and webhook body templates are Go
text/templates. Each can read:
| In the template | Is |
|---|---|
{{ .trigger.… }} |
The payload that fired the trigger. For a correlation, the correlation message |
{{ .steps.{step name}.… }} |
An earlier step's output, reached into by path when it is JSON |
{{ .{name} }} |
A value the step mapped into name |
So a Slack step needs no mappings to say
{{ .trigger.employee.name }} left on {{ .trigger.effective_date }}. If a step
maps a value into a name of its own called trigger or steps, that mapping
wins.
Two functions help: json renders a value as JSON ({{ json .trigger.matchedFields }}),
and default supplies a fallback ({{ default "unknown" .trigger.manager }}).
What to know:
- In email and Slack, a missing value fails the step.
{{ .trigger.nope }}stops the step with an error naming the key, rather than sending a message that says<no value>. The step's failure policy then applies. Webhook bodies keep rendering<no value>, as they always have. - Slack escapes the data, not your template.
&,<and>in payload values arrive as text, so an event containing<!channel>can't mention a channel. Links and mentions you write yourself still work. - Email subjects are one line. Line breaks in rendered data are replaced with spaces, so a payload can't add a header.
- Secrets are never read from data.
${secret.NAME}in an action's configuration is resolved before rendering; the same text inside a payload is sent as text. - To write a literal
{{, write{{"{{"}}.
http_webhook
Makes HTTP requests to configured URLs with templated payloads.
| Field | Type | Required | Description |
|---|---|---|---|
url |
string | yes | Target URL |
method |
string | no | HTTP method, defaults to POST |
headers |
map | no | Additional request headers |
body_template |
string | no | Go text/template for the request body. body is accepted too |
timeout_seconds |
number | no | Request timeout, defaults to 30 |
With a body_template, the template renders the body, so {{ .user_email }}
renders the value the step mapped into user_email and {{ .trigger.… }} reads
the event (see Templates). Without one, the input is sent as JSON. Either way the request carries
Content-Type: application/json, and X-RunDis-Run and X-RunDis-Hop naming
the run it belongs to — which is how a post back into one of your own
inbound webhooks is
recognised and bounded. A configured header of the same name does not override
them.
A response status of 400 or above fails the step. The response body is recorded as the step's output, truncated at 1 MB.
email
Sends emails via SMTP.
| Field | Type | Required | Description |
|---|---|---|---|
to |
list of strings | yes | Recipient email addresses. A comma-separated string, or the key recipients, is accepted too |
subject |
string | yes | Email subject line, a template |
body |
string | yes | Email body (plain text), a template |
cc |
list of strings | no | CC recipients. A comma-separated string is accepted too |
The subject and body are templates (see Templates), and the body is sent as plain text. On success the step's output names the recipients, so run history shows where a message actually went:
email sent to ops@example.com, manager@example.comslack_message
Posts messages to Slack via an incoming webhook URL.
| Field | Type | Required | Description |
|---|---|---|---|
webhook_url |
string | yes | Slack incoming webhook URL |
text |
string | yes | Message text, a template. message is accepted too |
channel |
string | no | Override channel |
username |
string | no | Override bot username |
icon_emoji |
string | no | Override bot icon |
The text is a template (see Templates). The three overrides are honoured only if the Slack webhook you created permits them; that is Slack's rule, not ours.
script
Runs a shell command in a container of its own. On hosted RunDis each run is a
short-lived Kubernetes Job from a small image (BusyBox, curl, jq, CA
certificates) that holds nothing of RunDis: no credentials, no service-account
token, a read-only file system apart from /work and /tmp, and network access
to the internet only — not to RunDis's own database, event bus or cluster, and
not to the nodes or RunDis's own public endpoint. The Jobs run in a namespace
of their own under Kubernetes' restricted Pod Security profile, where RunDis
itself can read only script pods' logs. A container is not a virtual machine;
it is a separate, unprivileged process tree, and that is what we claim.
Only admins can create a script action or edit its command: it is arbitrary code run from RunDis's address. Runs start a few seconds after the step does (a pod has to be scheduled), and at most a handful run at once; further runs wait briefly and then fail as busy.
| Field | Type | Required | Description |
|---|---|---|---|
command |
string | yes | The shell command or script to execute |
timeout_seconds |
number | no | Max execution time, defaults to 30 |
working_dir |
string | no | Working directory for the script |
env |
map | no | Environment variables to set |
The step's input arrives as environment variables, uppercased and prefixed with
INPUT_ — a step that maps user_email exposes $INPUT_USER_EMAIL.
These limits are not configurable.
- Runs under
/bin/shwith a restrictedPATHof/usr/local/bin:/usr/bin:/bin;working_dirmust be/workor under it - The timeout is enforced by the runtime, not by the script
- The process gets its own process group, so a timeout kills its children too
Standard output becomes the step's output, capped at 1 MB, with anything on
standard error appended after a STDERR: marker. The command's real exit code
is preserved, so a non-zero exit fails the step.
api_call
Makes authenticated API calls to provider endpoints. Like http_webhook, every
request carries X-RunDis-Run and X-RunDis-Hop.
| Field | Type | Required | Description |
|---|---|---|---|
url |
string | yes | API endpoint URL |
method |
string | no | HTTP method, defaults to GET |
headers |
map | no | Additional request headers |
body |
map | no | JSON request body |
auth_type |
string | no | bearer, basic, or api_key |
auth_token |
string | no | The credential, or ${secret.NAME} to reference a secret |
api_key_header |
string | no | Header name for api_key auth, defaults to X-API-Key |
timeout_seconds |
number | no | Request timeout, defaults to 30 |
The three authentication modes each read auth_token and do something different
with it:
bearersends it asAuthorization: Bearer <token>basicbase64-encodes it as given, so supply it already inuser:passwordformapi_keysends it in the header named byapi_key_header
Prefer a secret over the credential itself. Writing
${secret.STRIPE_API_KEY} here keeps the configuration readable by anyone who
needs to understand the action without handing them the token — and a credential
written directly is redacted when the configuration is read back, so it cannot be
retrieved afterwards anyway.
For POST, PUT and PATCH with no body configured, the step's input is sent
as the JSON body instead.
Choosing between http_webhook and api_call
They overlap. The distinction worth drawing is that http_webhook is built for
sending — it defaults to POST and gives you a template for shaping the
payload — while api_call is built for calling — it defaults to GET and
handles authentication for you.
If you are pushing data somewhere and need control over the body, use
http_webhook. If you are talking to an authenticated API, use api_call.