Skill, Workflow, And Subagent Tools

Copy Markdown View Source

This guide explains three operation sources that share one spine: skills contribute prompt instructions plus action-backed operations from a Jido.AI skill manifest, workflows expose deterministic application code as one model-callable operation, and subagents delegate a bounded task to a child agent within a single turn. All three compile to Jidoka.Agent.Spec.Operation entries and are routed through Jidoka.Operation.Source, so the turn loop sees a single operation model. By the end you will know which entity to reach for in each situation and how to test all three deterministically.

When To Use This

  • Use skill when you want a reusable bundle: prompt text plus the actions needed to honour that prompt. Skills are the right shape when the same capability ships to many agents and you want the prompt and tools to travel together.
  • Use workflow when application-owned deterministic code should appear as one tool. Workflows hide multi-step deterministic logic from the model.
  • Use subagent when a bounded specialist should answer one nested question inside the current turn, with its own model, instructions, and tools, and return a structured result to the parent.
  • Do not use subagent to transfer conversation ownership. That is a handoff and lives in Handoffs.
  • Do not use workflow for one-off code. A plain Jidoka.Action is simpler.

Prerequisites

  • A working Jidoka DSL agent. See Getting Started.
  • For skills: Jido.AI.Skill (vendored under jido_ai) and any skill modules or SKILL.md files you want to load.
  • No extra dependencies for workflows or subagents.

Quick Example

One DSL block can register all three sources for one agent:

defmodule MyApp.MathWorkflow do
  use Jidoka.Workflow,
    id: :math_workflow,
    description: "Adds one and doubles the value.",
    parameters_schema: %{
      "type" => "object",
      "properties" => %{"value" => %{"type" => "integer"}},
      "required" => ["value"]
    }

  @impl true
  def run(input, _context) do
    value = Map.get(input, :value) || Map.get(input, "value")
    {:ok, %{value: (value + 1) * 2}}
  end
end

defmodule MyApp.EvidenceAgent do
  use Jidoka.Agent

  agent :evidence_agent do
    instructions "Answer with bounded evidence."
  end
end

defmodule MyApp.HelperAgent do
  use Jidoka.Agent

  agent :helper_agent do
    instructions "Use available tools before answering."
  end

  tools do
    skill MyApp.SupportPolicySkill
    workflow MyApp.MathWorkflow, as: :run_math, result: :structured
    subagent MyApp.EvidenceAgent, as: :evidence_specialist, result: :structured
  end
end

The compiled spec exposes one operation per skill action, one run_math operation for the workflow, and one evidence_specialist operation for the subagent.

Concepts

╭──────────────────────────────╮
│ tools do                     │
│   skill MySkill              │
│   workflow MyFlow, ...       │
│   subagent ChildAgent, ...   │
╰──────────────┬───────────────╯
               │ Tool-source compiler.operations!
               ▼
╭──────────────────────────────╮
│ Jidoka.Operation.Source.*    │
│  Skill -> JidoActions path   │
│  Workflow -> WorkflowSource  │
│  Subagent -> SubagentSource  │
╰──────────────┬───────────────╯
               │ normalize
               ▼
╭──────────────────────────────╮
│ Jidoka.Agent.Spec.Operation  │
│  metadata.source =           │
│   "skill" | "workflow"       │
│   | "subagent"               │
╰──────────────┬───────────────╯
               │ Jidoka.Operation.Source.compile
               ▼
╭──────────────────────────────╮
│ Routed runtime capability    │
│  intent.name -> source       │
╰──────────────────────────────╯

Three things are the same across all three sources:

  1. One operation per call site. A workflow is always one operation. A subagent is always one operation. A skill is one operation per action the skill manifest publishes.
  2. Stable metadata. Every operation tags metadata.source with the string "skill", "workflow", or "subagent", plus kind, the underlying module, and a parameters schema when one is available.
  3. One capability per source. Jidoka.Operation.Source.compile/2 builds a router so the agent loop dispatches to the right source by operation name.

Differences worth keeping in mind:

  • Skill validates references through Jidoka.Skill.validate_ref/1. Module references must implement manifest/0, body/0, and actions/0. String references must match the lowercase-with-hyphens skill name format.
  • Workflow requires a module that use Jidoka.Workflow. Jidoka calls definition/1 at compile time to capture the id, description, and parameters schema.
  • Subagent requires a module that use Jidoka.Agent. The child agent is resolved at compile time, but its turn runs through the same harness as the parent's.

Security / Trust Boundaries

  • The DSL trusts skill, workflow, and subagent module references. Never derive them from user input. Resolve through an internal allowlist or registry first.
  • Jido.AI.Skill.Registry loads SKILL.md files relative to the agent's source directory. The DSL never expands paths supplied by external input; it only expands paths from load_path entries inside the DSL.
  • Subagent operations carry forward_context: policy. :public forwards the parent's public context to the child; {:only, keys} and {:except, keys} let you carve out what the child may see. Sensitive keys belong in the internal context and stay there by default.
  • Workflow operations are deterministic application code. Treat them as the safe alternative to ad hoc tool integrations: the workflow module is the one place that touches external systems, and it can validate inputs before it does.
  • None of these sources expose provider credentials. Subagents reach for credentials through the host environment, the same way the parent does.

How To

Step 1: Add A Skill For Reusable Prompt + Actions

A skill bundles instructions and action modules. Registering it contributes both.

defmodule MyApp.SupportPolicySkill do
  use Jido.AI.Skill,
    name: "support-policy",
    description: "Adds support policy lookup behavior.",
    allowed_tools: ["skill_policy_lookup"],
    actions: [MyApp.PolicyLookupAction],
    body: """
    # Support Policy

    Use skill_policy_lookup before answering policy questions.
    """
end

defmodule MyApp.SkillAgent do
  use Jidoka.Agent

  agent :skill_agent do
    instructions "Answer support questions with available capabilities."
  end

  tools do
    skill MyApp.SupportPolicySkill
  end
end

The compiled spec carries the skill body inside spec.instructions and one :skill operation per action in the manifest.

Step 2: Define A Deterministic Workflow

A workflow is one operation backed by code you fully own.

defmodule MyApp.RefundWorkflow do
  use Jidoka.Workflow,
    id: :process_refund,
    description: "Validates and queues a refund."

  @impl true
  def run(input, _context) do
    case input do
      %{"order_id" => order_id} when is_binary(order_id) ->
        {:ok, %{refund_id: "refund-#{order_id}", status: "queued"}}

      _ ->
        {:error, :missing_order_id}
    end
  end
end

tools do
  workflow MyApp.RefundWorkflow,
    as: :run_refund,
    forward_context: {:only, [:actor, :tenant]},
    result: :structured
end

result: :structured returns the workflow's {:ok, value} as the operation result. result: :output (the default) wraps the value with a basic envelope that the parent prompt can show verbatim.

Step 3: Delegate To A Subagent For One Task

A subagent is one bounded child turn. The parent decides when to call it; the child returns a structured result.

defmodule MyApp.EvidenceAgent do
  use Jidoka.Agent

  agent :evidence_agent do
    instructions "Return bounded evidence for a single question."
  end
end

defmodule MyApp.ParentAgent do
  use Jidoka.Agent

  agent :parent_agent do
    instructions "Delegate evidence collection before answering."
  end

  tools do
    subagent MyApp.EvidenceAgent,
      as: :evidence_specialist,
      timeout: 15_000,
      forward_context: {:only, [:tenant]},
      result: :structured
  end
end

The parent's prompt sees one tool, evidence_specialist. The child's turn runs through the same harness, with its own loop budget and timeout.

Step 4: Inspect The Compiled Operations

The spec metadata documents what was registered. Use it to confirm sources are configured the way you expect.

spec = MyApp.HelperAgent.spec()

Enum.map(spec.operations, & &1.name)
#=> ["skill_policy_lookup", "run_math", "evidence_specialist"]

spec.metadata["tool_sources"]
#=> [
#  %{"source" => "skill", "name" => "support-policy"},
#  %{"source" => "workflow", "name" => "run_math", "workflow" => "math_workflow"},
#  %{"source" => "subagent", "name" => "evidence_specialist"}
#]

Step 5: Choose Between Subagent And Handoff

Subagents and handoffs look similar but solve different problems:

ConcernSubagentHandoff
LifetimeOne bounded call within the parent's turnFuture turns for the conversation
OwnershipParent keeps ownership; child returns a resultOwnership transfers to the target agent
DSL entitysubagent ChildAgent, as: ...handoff TargetAgent, as: ...
Idempotency:idempotent by default:unsafe_once
When to use"Answer this side question for me""From now on, billing owns this thread"

When in doubt, ask whether the parent should still own the conversation after the call returns. If yes, use a subagent. See Handoffs for the handoff path.

Common Patterns

  • Pair a skill with a workflow. The skill teaches the prompt how to call a deterministic capability; the workflow implements it. The agent author composes both with two DSL lines.
  • Pin subagent results to :structured for downstream chaining. Plain text results are convenient for prompts but lose machine-readable shape.
  • Use forward_context: {:only, ...} to make subagent context a deliberate contract instead of an accident.
  • Treat workflow modules as bounded contexts. One workflow per business operation is easier to test and review than a giant module with branching.

Testing

All three sources use the same fake-LLM pattern as Getting Started. The interesting differences are at the operation boundary:

defmodule MyApp.HelperAgentTest do
  use ExUnit.Case, async: true

  test "workflow round trip" do
    llm = fn _intent, journal ->
      llm_calls = Enum.count(journal.results, fn {_id, r} -> r.kind == :llm end)

      case llm_calls do
        0 -> {:ok, %{type: :operation, name: "run_math", arguments: %{"value" => 5}}}
        1 -> {:ok, %{type: :final, content: "The result is 12."}}
      end
    end

    assert {:ok, result} =
             Jidoka.turn(MyApp.HelperAgent, "Compute next.", llm: llm)

    assert result.content =~ "12"
  end
end

Subagent tests use the same shape; the inner agent's prompt is exercised by the same fake LLM through pattern matching on payload.agent_id. See test/jidoka/subagent_test.exs for the canonical example. Skill tests live in test/jidoka/skill_test.exs; workflow tests in test/jidoka/workflow_test.exs.

Troubleshooting

SymptomLikely CauseFix
{:error, {:invalid_skill, ref, reason}} at compile timeThe skill module or name failed validation.Confirm the module exports manifest/0, body/0, and actions/0, or use a hyphenated string name registered through Jido.AI.Skill.Registry.
{:error, {:invalid_workflow_module, module, reason}}The workflow module is missing run/2 or has a non-string id.use Jidoka.Workflow, id: :snake_case_id and implement run/2.
{:error, {:duplicate_operation_source_name, name}}Two sources produced the same operation name.Use as: overrides on workflow or subagent, or split the agent.
Subagent times outtimeout: is too small for the child's tool loop.Raise timeout: or simplify the child. The default is 30_000 ms.
Skill prompt does not appear in spec.instructionsThe skill module failed to resolve; check Jidoka.Skill.prompt/2 for the same refs.Add a load_path entry or register the skill at the application layer.

Reference

Key modules touched in this guide: