Skip to main content

guides/skill-workflow-subagent-tools.md

# Skill, Workflow, And Subagent Tools

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](handoffs.md).
- Do **not** use **workflow** for one-off code. A plain `Jidoka.Action`
  is simpler.

## Prerequisites

- A working Jidoka DSL agent. See [Getting Started](getting-started.md).
- 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:

```elixir
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

```diagram
╭──────────────────────────────╮
│ 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.

```elixir
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.

```elixir
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.

```elixir
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.

```elixir
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:

| Concern | Subagent | Handoff |
| --- | --- | --- |
| Lifetime | One bounded call within the parent's turn | Future turns for the conversation |
| Ownership | Parent keeps ownership; child returns a result | Ownership transfers to the target agent |
| DSL entity | `subagent 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](handoffs.md)
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](getting-started.md). The interesting differences are at the
operation boundary:

```elixir
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

| Symptom | Likely Cause | Fix |
| --- | --- | --- |
| `{:error, {:invalid_skill, ref, reason}}` at compile time | The 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 out | `timeout:` 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.instructions` | The 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:

- [`Jidoka.Operation.Source`](`Jidoka.Operation.Source`) - behaviour and
  compiler that all three sources share.
- [`Jidoka.Skill`](`Jidoka.Skill`) - skill validation, action extraction,
  prompt rendering, and metadata.
- [`Jidoka.Workflow`](`Jidoka.Workflow`) - behaviour for deterministic
  workflow modules.
- [`Jidoka.Operation.Source.Workflow`](`Jidoka.Operation.Source.Workflow`) -
  workflow source struct and capability.
- [`Jidoka.Operation.Source.Subagent`](`Jidoka.Operation.Source.Subagent`) -
  subagent source struct and capability.
- [`Jidoka.Agent.Spec.Operation`](`Jidoka.Agent.Spec.Operation`) - the
  normalized operation entry produced by every source.

## Related Guides

- [Getting Started](getting-started.md) - the smallest DSL agent end to end.
- [Handoffs](handoffs.md) - conversation ownership transfer; the partner
  pattern to subagents.
- [AshJido Resources](ash-jido.md) - a sibling source for Ash-backed tools.
- [MCP Tools](mcp-tools.md) - a sibling source for remote MCP servers.
- [Browser Tools](browser-tools.md) - a sibling source for constrained
  browsing.