Skip to content

LLM Overview ​

Call a model from a step with action: chat.completion, and go further from there: let the completion call workflows as functions, let a model decide which step runs next, run a full coding agent as a step, or let an external AI operate Dagu.

Where a model fits ​

  • A model call inside a step: action: chat.completion sends a prompt or message list; the response streams to stdout. This page.
  • Workflows as functions: a completion with tools can call your DAGs, with arguments derived from their params. See Tool Calling.
  • A model deciding what runs: type: agent inverts control; steps become a catalog and the model picks one or more independent actions per turn until the goals are met. See Agent DAGs and the Agent DAG examples.
  • Typed answers instead of prose: action: decision.evaluate asks a model several questions about the same material and returns a selected option, a score, or a probability, each with its own confidence. See Decision Steps.
  • A coding agent as a step: harness.run launches supported coding-agent CLIs such as Claude Code, Codex, Gemini CLI, Cursor, or DeepSeek Harness inside a workflow. See Harness.
  • AI operating Dagu: the MCP server is the inverse relationship; an external AI client inspects workflows, starts runs, and reads results.

The examples below run as-is with an OPENROUTER_API_KEY exported; swap the llm block for any configured provider.

First completion ​

yaml
secrets:
  - name: OPENROUTER_API_KEY
    provider: env
    key: OPENROUTER_API_KEY

llm:
  provider: openrouter
  model: deepseek/deepseek-v4-flash

steps:
  - id: ask
    action: chat.completion
    with:
      prompt: |
        What is 2+2? Reply with just the number.
    output: ANSWER

  - id: use_answer
    run: echo "The model said ${ANSWER}"
    depends: ask

Three things carry this example:

  • The secrets entry reads the key from the Dagu process environment and masks it in logs. Exporting the variable alone is not enough, because only a small set of environment variables passes through to workflows.
  • The DAG-level llm block is inherited by every chat step that sets no LLM fields of its own, so steps stay small.
  • output: ANSWER captures the response for any dependent step as ${ANSWER}. Object-form output namespaces it to the step instead (response: { from: stdout }, read as ${ask.output.response}), and a large response can stream straight to an artifact.

Messages and system prompts ​

Use messages when the request needs an explicit conversation, and system to set the role:

yaml
steps:
  - id: diagnose
    action: chat.completion
    with:
      provider: openrouter
      model: deepseek/deepseek-v4-flash
      system: |
        Answer as a concise operations runbook author.
      messages:
        - role: user
          content: |
            Explain how to diagnose a saturated connection pool.
            Keep it under 80 words.

Specify either prompt or messages; prompt is converted to one user message and takes precedence when both are present. Roles can be system, user, or assistant.

Note the explicit provider and model here: when an action sets any LLM field under with (even just system), its configuration replaces the complete DAG-level llm block rather than merging with it.

What a completion can do ​

Each capability below is one field away. Snippets show the shape; the AI examples page has full runnable versions of each.

Multi-turn sessions. Chat steps inherit the conversation from the steps they depend on:

yaml
  - id: follow_up
    action: chat.completion
    with:
      prompt: Multiply that by 3.
    depends: ask

See Sessions.

Call workflows as tools. Names in tools expose DAGs as functions; each call is a real child run:

yaml
    with:
      tools:
        - calculator
      prompt: What is 15 times 23? Use the calculator tool.

See Tool Calling.

Typed answers. A step-level output_schema makes the model answer in that shape, and each listed property becomes a step output:

yaml
  - id: classify
    action: chat.completion
    with:
      prompt: "Classify this note: ${params.note}"
    output_schema:
      type: object
      properties:
        category: {type: string, enum: [refund, complaint, question]}
      required: [category]

See Structured output.

Read files. files hands the model PDFs, Word and Excel documents, text files, and images with the prompt:

yaml
    with:
      prompt: Read the invoice and give its number, date, and total.
      files: ~/Downloads/invoice-*.pdf

See Files.

Route on the answer. Ask for a label, then branch with router.route:

yaml
  - id: route_request
    depends: [classify_request]
    action: router.route
    with:
      value: ${REQUEST_TYPE}
      routes:
        bug: [handle_bug]
        feature: [handle_feature]

Constrain the model to known labels and keep an explicit fallback route; a response that matches no route runs no handler. See Router.

When the answer is a label rather than prose, decision.evaluate does this without the hand-constraining: the options are declared as criteria, the answer is always one of them, and it comes back with a confidence you can gate on with a numeric comparison.

Extended reasoning. thinking maps to the provider's reasoning controls:

yaml
    with:
      thinking:
        enabled: true
        effort: low

See Reasoning.

Model fallback. An ordered model list tries the next entry after retries are exhausted:

yaml
    with:
      model:
        - provider: openrouter
          name: deepseek/deepseek-v4
        - provider: openrouter
          name: deepseek/deepseek-v4-flash

See Reliability.

Web search. Provider-integrated search grounds the answer in current results:

yaml
    with:
      web_search:
        enabled: true
        max_uses: 3

See Web Search.

Structured output ​

Set output_schema on the step, next to with, when later steps need values rather than prose:

yaml
steps:
  - id: classify
    action: chat.completion
    with:
      prompt: |
        Classify this customer note and extract the amount:
        I was charged twice, please refund the extra 12.50 EUR.
    output_schema:
      type: object
      properties:
        category:
          type: string
          enum: [refund, complaint, question]
        amount:
          type: number
        currency:
          type: string
      required: [category]

  - id: record
    depends: classify
    run: echo "${steps.classify.outputs.category} ${steps.classify.outputs.amount}"
  • The model answers by calling a respond tool whose parameters are the schema, and the request requires a tool call. A reply that is a JSON object in plain text is accepted too.
  • Properties the schema does not list are dropped, and the rest is validated against the schema. Stdout is one JSON object, and each listed property becomes ${steps.<id>.outputs.<name>}. Referencing a name the schema does not list is reported as unknown when the DAG loads.
  • An answer that does not match gets one correction, sent with the reason. If it still does not match, the next entry of a model list is tried. When no model answers, the step fails with the model gave no answer that matches output_schema; the step's stderr shows each rejected answer and why.
  • The schema must declare type: object, list at least one property, and list every required name under properties. It cannot be combined with web_search or with a tool named respond.
  • With tools, the tool workflows are offered next to respond, and the loop ends when the model calls respond. Reaching max_tool_iterations without an answer counts as that model failing: the next entry of a model list starts over, running the tools again, and the step fails when no model is left.
  • The answer is never streamed.

Validation checks the shape of the answer, not whether it is true. For extraction, leave a field out of required when the input may not contain it: a required field makes the model invent a value rather than leave it out.

Some models refuse a forced tool call: Claude models while thinking is enabled, and Claude 5 models always. For those, Dagu asks for the tool without forcing it and relies on the instruction, the validation, and the correction.

Files ​

files names the files the model reads with the prompt: one path or glob pattern, several of them as a list or one per line, or a list an earlier step published, such as the messages of a mail.search step that saved attachments.

yaml
steps:
  - id: read
    action: chat.completion
    with:
      provider: anthropic
      model: claude-sonnet-5
      prompt: Read the invoice and give its number, date, and total.
      files: ~/Downloads/invoice-*.pdf
    output_schema:
      type: object
      properties:
        number: {type: string}
        date: {type: string}
        total: {type: number}
      required: [number]
  • A leading ~ is the home directory, and a relative path resolves from the step's working directory. Each file is read once, in the order named, before any request.
  • Text files (.txt, .md, .csv, .tsv, .json, .jsonl, .yaml, .yml, .xml, .html, .htm, .log), Word documents (.docx), and Excel workbooks (.xlsx, .xlsm) are given as text, a paragraph or row a line. A text file in Shift_JIS is decoded.
  • A PDF is given page by page as text. A page without text of its own, such as a scan, is rendered to an image, which the text names as --- page 2: image 1 ---.
  • PNG, JPEG, GIF, and WebP files are given as images, scaled down to a long edge of 1568 pixels when larger. Choose a model that accepts images when a file is an image or a scan.
  • The text goes ahead of the last user message, each file in a <file name="…"> block. The images go with that message, and the text names each by its place, such as [Image 1 is receipt.png.].
  • Together the files may hold up to 1,000,000 characters of text and 50 images. A file over 50 MB, a file of another kind, a missing file, and a pattern that matches nothing fail the step before any request.
  • The session keeps the text, so a later chat step that inherits the session still reads it. The images go with the one request and are not kept. A push-back resends the feedback without the files.

files is an LLM configuration field: a step that sets it replaces the DAG-level llm block, so it needs its own provider and model. A DAG-level llm block rejects files, since files belong with the prompt that asks about them.

Sessions ​

The completed session is saved with the DAG run, including the provider, model, and token usage reported for assistant messages. A chat step also inherits conversation history from the steps in its depends list, which is what makes the multi-turn pattern above work:

  • History is transitive: every chat step saves its inherited messages, its own messages, and the response.
  • Histories from multiple dependencies are merged in the order listed in depends.
  • Only the first system message is kept when inherited histories contain several.
  • Retries continue with the session already attached to the DAG run.

When approval push-back or human-task push-back re-executes a chat step, Dagu restores the previous conversation and appends the reviewer feedback as the next user message; the workflow does not need to wire ${FEEDBACK} itself.

Model usage ​

Each step that calls a model keeps what it used, over every attempt, for each provider and model: the number of calls, input tokens with the cached and cache-write tokens among them, output tokens with the reasoning tokens among them, and the cost the provider reported. The run details from the REST API give it as modelUsage on each step, and summed over every step and handler as modelUsage on the run.

  • Chat completions, Agent DAG decisions, decision steps, and the model calls of browser, computer, and xlsx steps count toward their own step.
  • A sub-DAG step counts what its child run used, and a foreach step counts what its item bodies used.
  • A managed OpenCode step counts what its session reports. Other coding-agent CLIs and ACP agents do not report usage; the step names such a tool in notReported.
  • Cost appears only when the provider reports it, as OpenRouter does. costReportedCalls counts the calls that reported a cost, so the cost is complete only when it equals calls.

Configuration ​

All action-specific fields belong under with, except output_schema, which sits on the step; see Structured output.

FieldTypeDefaultDescription
promptstring-A non-empty user prompt. Required when messages is omitted.
messagesarray-A non-empty list of {role, content} messages. Required when prompt is omitted.
providerstringinheritedLLM provider. Required for a string model unless inherited; omit it when every fallback model entry has its own provider.
modelstring or arrayinheritedModel identifier, or an ordered list of model configurations for fallback. Inherited only when the action sets no LLM configuration fields.
systemstring-Default system prompt. An explicit system message in messages takes precedence.
temperaturenumberprovider defaultSampling randomness from 0.0 to 2.0.
max_tokensintegerprovider defaultMaximum number of tokens to generate.
top_pnumberprovider defaultNucleus sampling value from 0.0 to 1.0.
base_urlstringprovider defaultBase URL for a custom or OpenAI-compatible endpoint.
api_key_namestringprovider defaultEnvironment variable that contains the API key.
provider_orderarray-OpenRouter only: the upstream providers to try first, in order.
streambooleantrueStream response tokens to stdout as they arrive.
thinkingobjectdisabledProvider-specific extended reasoning.
toolsarray-DAG names exposed to the model as callable tools.
max_tool_iterationsinteger10Maximum tool-calling rounds, at least 1.
web_searchobjectdisabledBuilt-in web-search integration settings.
filesstring or array-Files the model reads with the prompt: paths, glob patterns, or a list an earlier step published. Not valid in the DAG-level llm block.

messages[].content, system, base_url, and files support scoped value references such as ${params.TOPIC} and ${env.LLM_BASE_URL}.

Security ​

Values registered as Dagu secrets are masked before messages are sent to the provider. The run's saved session retains the resolved message content, including the text read from files, so avoid placing unnecessary secrets in prompts and restrict access to run history.

Next Steps ​

  • AI Examples for runnable copy-paste versions of every capability above
  • Providers & Endpoints for credentials, custom endpoints, and shared defaults
  • Agent DAGs & Completions for the step-by-step path from one completion to LLM-directed workflows
  • Agent DAGs when the order of the work is itself the model's decision
  • Harness for running external coding agents as steps
  • Chat & LLM for choosing between the shapes at a glance

Dagu is open source under the GNU General Public License v3.0.