Custom Actions
actions defines reusable, typed actions that expand to built-in step types when a workflow is loaded. Use them when several DAGs should share the same validated call shape without copying executor-specific YAML everywhere.
Basic Shape
actions:
release.announce:
description: Print a reusable release announcement
input_schema:
type: object
additionalProperties: false
required: [channel, version]
properties:
channel:
type: string
enum: [changelog, email, slack]
version:
type: string
summary:
type: string
default: Ready for rollout
template:
run: echo {{ json .input.channel }} release {{ json .input.version }} - {{ json .input.summary }}
steps:
- id: build
output:
version: "v1.2.3"
- id: announce
action: release.announce
with:
channel: changelog
version: ${build.output.version}
depends: [build]The call site stays consistent:
actionchooses the reusable action.withis validated against the action'sinput_schema.- The rendered
templatebecomes the actual step that Dagu executes.
Where To Declare Actions
- At the top level of a DAG document.
- In
base.yaml, so every DAG can inherit the same action definitions. - DAG-local actions are visible only inside the YAML document that declares them.
- Base-config and DAG-local actions are merged per YAML document.
- A DAG-local action that duplicates a base-config action name is rejected.
Definition Fields
| Field | Required | Description |
|---|---|---|
description | No | Human-readable description copied to expanded steps unless the call site overrides it. |
input_schema | Yes | Inline JSON Schema object for with. The schema must resolve to an object schema. |
output_schema | No | Inline JSON Schema object for JSON stdout validation. |
template | Yes | Canonical step fragment using run or action plus workflow-control fields. |
Action names must match:
^[A-Za-z][A-Za-z0-9_-]*(\.[A-Za-z][A-Za-z0-9_-]*)*$Custom action names cannot reuse built-in step type names such as http.request, dag.run, dag.enqueue, template.render, or agent.run.
Template Rules
String values inside template are rendered with Go text/template using .input as the validated input object. Custom action templates expose the same template functions as Template, plus json, which returns a JSON-safe encoding of a value.
actions:
webhook.send:
input_schema:
type: object
additionalProperties: false
required: [url, text]
properties:
url:
type: string
text:
type: string
template:
action: http.request
with:
method: POST
url: "{{ .input.url }}"
headers:
Content-Type: application/json
body: |
{"text": {{ json .input.text }}}Rules:
- Missing template keys are errors.
- Template functions are hermetic; environment, network, clock, random, and crypto helpers are not available.
templatemust use canonical execution fields such asrunoraction. Legacy fields such ascommand,script,type,call,messages,agent,value, androutesare rejected for custom action templates.- Runtime expressions such as
${BUILD_ID}are ordinary text during template rendering and are evaluated later by the expanded step if they land in a runtime-evaluated field.
Typed Input Injection
Use $input when a rendered field should keep the original JSON type instead of becoming a string template result.
actions:
say.exec:
input_schema:
type: object
additionalProperties: false
required: [message]
properties:
message:
type: string
template:
action: exec
with:
command: /bin/echo
args:
- {$input: message}
steps:
- action: say.exec
with:
message: 'Review "quoted" text'$input paths are relative to the validated input object after schema defaults are applied. They support dotted object fields and numeric array indexes such as items.0.name.
Output Contracts
Use output_schema when a custom action emits machine-readable JSON:
actions:
ticket.classify:
input_schema:
type: object
additionalProperties: false
required: [text]
properties:
text:
type: string
output_schema:
type: object
additionalProperties: false
required: [category, priority]
properties:
category:
type: string
enum: [bug, feature, question]
priority:
type: string
enum: [low, medium, high]
template:
run: |
python3 - <<'PY'
import json
print(json.dumps({"category": "bug", "priority": "high"}))
PY
steps:
- id: classify
action: ticket.classify
with:
text: "App crashes on startup"
- id: route
depends: [classify]
run: echo "${classify.output.category}:${classify.output.priority}"Rules:
stdoutmust contain valid JSON that matchesoutput_schema.- Human-readable logs should go to
stderr. - If the call site does not set
output:, the decoded JSON object is published as${step_id.output.*}. - If the call site sets object-form
output:, Dagu validates stdout first, then applies the explicit output mapping.
Call-Site Fields
Allowed call-site fields are workflow-control fields:
id, name, description, depends, continue_on, retry_policy, repeat_policy,
mail_on_error, preconditions, signal_on_stop, env, timeout_sec, stdout,
stderr, log_output, worker_selector, output, approvalExecution fields belong in the action template, not at the call site. A custom action call uses only:
steps:
- action: release.announce
with:
channel: slack
version: v1.2.3Base Config Example
base.yaml
actions:
notify.success:
input_schema:
type: object
additionalProperties: false
required: [message]
properties:
message:
type: string
template:
action: log.write
with:
message: {$input: message}hello.yaml
steps:
- action: notify.success
with:
message: hello from baseLegacy step_types
step_types remains loadable for backward compatibility, but it is deprecated. dagu validate reports a deprecation warning for legacy definitions while still returning success when there are no real validation errors. New workflows should use actions.
