Custom Step Types
step_types defines reusable step types that expand to builtin-backed steps when a DAG is loaded. They are resolved during spec build, before normal step validation and execution. The runtime executes the expanded builtin step.
Where You Can Declare Them
- At the top level of a DAG document.
- In
base.yaml. - Base-config and DAG-local definitions are merged per YAML document.
- A DAG-local definition that duplicates a base-config name is rejected.
- A DAG-local definition is visible only inside the YAML document that declares it. Another document separated by
---must redeclare it or inherit it from base config.
Definition Format
step_types:
greet:
type: command
description: Print a greeting
input_schema:
type: object
additionalProperties: false
required: [message]
properties:
message:
type: string
repeat:
type: integer
default: 2
template:
script: |
#!/bin/bash
for ((i=0; i<{{ .input.repeat }}; i++)); do
printf '%s\n' {{ json .input.message }}
done
steps:
- type: greet
with:
message: helloThis expands to a builtin command step at load time. with.repeat defaults to 2, and because the template uses a shebang with no template.shell, Bash runs the script. If name is omitted, the generated name uses the custom type prefix, such as greet_1.
Definition Fields
step_types.<name>must match^[A-Za-z][A-Za-z0-9_-]*$.- Custom names cannot reuse builtin step type names such as
command,http,kubernetes,s3,chat, oragent. typeis required and must point to a builtin step type or builtin alias. Custom step types cannot target other custom step types.input_schemais required. It must be an inline JSON Schema object that resolves to an object schema.output_schemais optional. It must be an inline JSON Schema object that resolves to an object schema. When set, the expanded step's stdout must be valid JSON and match this schema when the step completes.templateis required. It must be a step fragment object.template.typeis rejected. The expanded builtin type always comes fromstep_types.<name>.type.descriptionis optional. It is applied only when the expanded step does not set its own description.
Output Contracts
Use output_schema when a custom step type should behave like a typed workflow component: input_schema validates what the caller passes in, and output_schema validates what the step promises to emit.
step_types:
classify_ticket:
type: command
input_schema:
type: object
additionalProperties: false
required: [text]
properties:
text:
type: string
output_schema:
type: object
additionalProperties: false
required: [category, priority, confidence]
properties:
category:
type: string
enum: [bug, feature, question]
priority:
type: string
enum: [low, medium, high]
confidence:
type: number
minimum: 0
maximum: 1
template:
script: |
#!/usr/bin/env python3
import json
print(json.dumps({"category": "bug", "priority": "high", "confidence": 0.91}))
steps:
- id: classify
type: classify_ticket
with:
text: "App crashes on startup"
- id: route
depends: [classify]
command: echo "${classify.output.category}:${classify.output.priority}"Rules:
stdoutmust contain machine-readable JSON. Write human-readable logs tostderr.- The decoded stdout JSON is validated against
output_schema. Invalid JSON or a schema mismatch fails the step, so normalretry_policy,continue_on, and handlers apply. - If the step does not set
output:, the validated decoded object is published as the step output and can be referenced with${step_id.output.*}. - If the step also sets object-form
output:, Dagu validates stdout first, then applies the explicit output mapping. - String-form
output: NAMEstill captures stdout into a flat variable after validation.
Template Rendering
String values inside template are rendered with Go text/template using .input as the template data. Custom step templates expose the same template functions as the Template step, plus a json helper that returns the JSON encoding of a value.
Available Functions
Custom step templates can use:
- Dagu pipeline-friendly functions documented for the template executor:
split,join,count,add,empty,upper,lower,trim, anddefault - The exact slim-sprig-derived function names listed on the Template step
json, which is specific to custom step templates and returns the JSON encoding of a value. This helper is useful when embedding user input inside a string field such asscriptor an HTTP body.
Functions that read environment variables, perform network lookups, use current time, generate random values, or generate crypto keys are not available. The exact blocked names are listed on the Template step.
step_types:
webhook:
type: http
input_schema:
type: object
additionalProperties: false
required: [url, text]
properties:
url:
type: string
text:
type: string
template:
command: POST {{ .input.url }}
with:
headers:
Content-Type: application/json
body: |
{"text": {{ json .input.text }}}Rules:
- Missing template keys are errors.
- Template functions are hermetic; functions for environment access, network lookup, time, randomness, and crypto key generation are not available.
- Schema defaults are applied to
with, then the result is validated, then the template is rendered during DAG load.
Typed Input Injection
Use $input when a rendered field should be copied directly from custom-step with instead of rendered as a string template.
At the call site, with becomes the custom step input:
steps:
- type: say
with:
message: 'Review "quoted" text'Inside template, this copies the validated message value into the rendered step:
template:
exec:
command: /bin/echo
args:
- {$input: message}The expanded builtin step receives:
exec:
command: /bin/echo
args:
- 'Review "quoted" text'$input path resolution is relative to the validated input object after schema defaults are applied. It supports dotted object fields and numeric array indexes such as items.0.name.
Use $input for whole-field values such as argv entries, prompts, booleans, numbers, arrays, or objects. The copied value keeps its type and is not parsed as Go template text. For embedded text, use normal Go template syntax inside a string field.
Runtime Expressions
Custom step templates are rendered while the DAG is loaded, before a step starts running. They are not rendered again when the step executes. This means runtime values cannot change Go template control flow and cannot change the static step graph.
Rule of thumb:
- Go template actions are custom-step template rendering at DAG load time.
${...}and$VARare Dagu runtime expressions evaluated later by the expanded builtin step.- Backticks survive custom-step expansion unchanged. What happens later depends on the destination field: runtime-evaluated fields still process backticks, while command-step
scriptleaves them for the shell. - Runtime expressions can be passed through custom step templates, but they cannot control Go template
if,range, or other load-time template logic.
Runtime expressions are still valid when they end up in fields that Dagu evaluates at execution time. ${...} and $VAR work in command strings, command arguments, scripts, and executor config strings. Backticks also continue to work in the fields that use normal runtime evaluation.
If a runtime expression is written directly in template, it is ordinary text during custom template rendering. For example, ${COUNT} is not Go template syntax, so it stays ${COUNT} in the expanded builtin step. It expands later when that builtin step executes, provided it is in a runtime-evaluated field.
type: graph
step_types:
echo_count:
type: command
input_schema:
type: object
additionalProperties: false
properties: {}
template:
exec:
command: /bin/echo
args:
- ${COUNT}
steps:
- id: produce
exec:
command: /bin/echo
args: [7]
output: COUNT
- id: consume
depends: [produce]
type: echo_count
output: OUTRuntime expressions can also come from custom-step with and be passed through the template:
type: graph
step_types:
repeat:
type: command
input_schema:
type: object
additionalProperties: false
required: [count]
properties:
count:
type: integer
template:
exec:
command: /bin/echo
args:
- {$input: count}
steps:
- id: produce
exec:
command: /bin/echo
args: [3]
output: COUNT
- id: consume
depends: [produce]
type: repeat
with:
count: ${COUNT}
output: OUTIn this example, with.count is declared as an integer, but ${COUNT} is accepted by load/save validation because it is a whole runtime expression. The template injects the literal string ${COUNT} into the expanded builtin step. The command executor evaluates it when consume runs, after produce has written COUNT.
Validation rules for runtime expressions in custom with input are intentionally narrow:
- String schema fields can contain embedded runtime expressions, such as
prefix-${NAME}. - Integer, number, boolean, and scalar enum fields can use a runtime expression only as the whole value, such as
${COUNT},$COUNT, or`cat count.txt`. - Nested object properties and array items follow the same rule when their schema declares one of those scalar forms.
- Unknown fields, missing required fields, invalid additional properties, and non-runtime invalid values are still rejected.
- Custom input schema validation is not repeated after the runtime expression expands. The expanded builtin step and executor handle the final runtime value.
Script Templates
For command-backed or shell-backed custom step types, template.script is usually the simplest and most common option.
step_types:
bash_snippet:
type: command
input_schema:
type: object
additionalProperties: false
required: [message]
properties:
message:
type: string
template:
script: |
#!/bin/bash
printf '%s\n' {{ json .input.message }}
steps:
- type: bash_snippet
with:
message: xxxRules:
- Put
scriptin the custom steptemplate, not at the custom-step call site. - If
template.scripthas a shebang and you do not settemplate.shell, the shebang interpreter is used. - If you set
template.shell, that shell runs the script instead, so the shebang is not used for interpreter selection.
Call-Site Fields
When a step uses a custom type, with is input to the custom definition. It is not merged directly into builtin executor configuration.
Allowed call-site fields: name, id, 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, approval.
Rejected call-site fields: command, exec, script, shell, shell_packages, working_dir, call, params, parallel, container, llm, messages, agent, value, routes.
If you need one of the rejected fields, put it in template.
Precedence for custom step expansion is:
- Explicit allowed call-site fields override the rendered template.
- Explicit fields in the rendered template override DAG or base-config
defaults. - Additive fields compose in this order:
defaults, thentemplate, then explicit call-site values.
For additive fields, this means:
enventries fromdefaultsare prepended beforetemplate.env, and explicit call-siteenventries are appended last.preconditionsfromdefaultsrun beforetemplate.preconditions, and explicit call-sitepreconditionsrun last.
Base Config Example
base.yaml
step_types:
greet:
type: command
input_schema:
type: object
additionalProperties: false
required: [message]
properties:
message:
type: string
template:
script: |
#!/bin/bash
printf '%s\n' {{ json .input.message }}hello.yaml
steps:
- type: greet
with:
message: hello from baseEvery DAG loaded with that base config can use type: greet.
Handlers
Custom step types can be used in steps and in handler_on.
step_types:
notify:
type: http
input_schema:
type: object
additionalProperties: false
required: [url, text]
properties:
url:
type: string
text:
type: string
template:
command: POST {{ .input.url }}
with:
headers:
Content-Type: application/json
body: |
{"text": {{ json .input.text }}}
handler_on:
success:
type: notify
with:
url: https://hooks.example.com/workflow
text: completedDirect Exec
For command-backed or shell-backed custom step templates, use exec when you want explicit argv with no shell parsing.
steps:
- exec:
command: /usr/bin/python3
args:
- -u
- script.py
- --limit
- 10For custom step types whose type is command or shell, template.exec is valid. It is mutually exclusive with command and script, and it cannot be combined with shell or shell_packages.
