Jidoka.Operation.Source is the single seam between the runtime and any
external operation surface (Jido actions, Ash resources, browsers, MCP
servers, sub-agents, workflows). Every source compiles to the same two
outputs: a list of Jidoka.Agent.Spec.Operation data and one runtime
capability function. This guide documents that contract and shows how the
built-in sources adopt it.
When To Use This
- Use this guide when authoring a new operation source (custom backend, internal tool registry, third-party SDK adapter).
- Use this guide when wiring multiple sources together into one agent.
- Do not use this guide for authoring individual operations or actions; for
that see Agent DSL and
Jidoka.Action.
Prerequisites
- You have read Agent Spec Contract and Turn And Effect Contracts.
- You can build and run a Jidoka turn.
Quick Example
Source.Local is the simplest source. It compiles a list of in-process
handlers into the same shape every other source returns.
alias Jidoka.Operation.Source
{:ok, source} =
Source.Local.new(
operations: [
%{
name: "local_time",
description: "Returns local time for a city.",
handler: fn args, _ctx ->
{:ok, %{city: Map.get(args, "city", "Chicago"), time: "09:30"}}
end
}
]
)
{:ok, compiled} = Source.compile(source)
compiled.operations
#=> [%Jidoka.Agent.Spec.Operation{name: "local_time", ...}]
compiled.capability
#=> #Function<...> (operation_capability/2)compiled.operations is what the spec stores. compiled.capability is what
the runtime invokes when an LLM decides to call an operation.
Concepts
╭───────────────╮ ╭──────────────────╮ ╭─────────────────────╮
│ Source struct │────▶│ Source.compile/1 │────▶│ %{operations, │
│ (Local, │ │ │ │ capability} │
│ Ash, MCP, │ ╰──────────────────╯ ╰──────────┬──────────╯
│ Browser, …) │ ▼
╰───────────────╯ ╭──────────────────────╮
│ Turn.State pending │
│ Effect.Intent │
│ └─ capability.(…) │
╰──────────────────────╯A source is a struct that implements the Jidoka.Operation.Source behaviour.
Two callbacks - operations/2 and capability/2 - return the data and the
function the runtime needs. Source.compile/1 validates name uniqueness
across multiple sources and produces a single routed capability.
Fields
compile/1 Output
Jidoka.Operation.Source.compile/2 returns {:ok, compiled()} where:
| Field | Type | Purpose |
|---|---|---|
operations | [Jidoka.Agent.Spec.Operation.t()] | Flat list across all sources, suitable for Agent.Spec.operations. |
capability | Jidoka.Runtime.Capabilities.operation_capability/0 | Routed function: looks up the source by operation name and forwards the intent. |
Duplicate operation names across sources fail with
{:error, {:duplicate_operation_source_name, name}}.
operation_capability/3 Signature
The capability is a three-arity function that mirrors the LLM capability shape:
@type operation_capability ::
(Jidoka.Effect.Intent.t(), Jidoka.Effect.Journal.t(), Jidoka.Context.t() ->
{:ok, term()} | {:error, term()})| Argument | Purpose |
|---|---|
Effect.Intent (kind :operation) | Carries the normalized Effect.OperationRequest payload, idempotency key, and id. |
Effect.Journal | Read-only view of recorded intents/results, used for replay-safety checks. |
The capability returns the raw operation output on success. The runtime wraps
that output into an Effect.Result and an Effect.OperationResult for you;
sources should not build those structs themselves.
Jidoka.Operation.Source Behaviour
Two callbacks form the contract:
| Callback | Purpose |
|---|---|
operations(source, opts) :: {:ok, [Spec.Operation.t()]} | {:error, term()} | Return the operation data the spec will store. |
capability(source, opts) :: {:ok, operation_capability()} | {:error, term()} | Return the executor function. |
Sources are plain structs. The first positional argument to each callback is a
%__MODULE__{}; the second is an opts keyword forwarded from
Source.compile/2.
Source.Local
In-process operation source for tests and lightweight tools.
| Field | Type | Purpose |
|---|---|---|
:operations | [operation_def()] | List of %{name, handler, description?, idempotency?, kind?, metadata?} entries. |
Handlers must be 2- or 3-arity functions returning {:ok, term()} or
{:error, term()}. See Jidoka.Operation.Source.Local.
Other Built-In Sources
All adopt the same Source behaviour:
Jidoka.Operation.Source.MCP- MCP server tools.Jidoka.Operation.Source.Subagent- nested Jidoka agent as a callable operation.Jidoka.Operation.Source.Handoff- hand-off operations.Jidoka.Operation.Source.Workflow- workflow-backed operations.
External integrations such as Ash/Jido and the browser source ship in their
own packages but compile to the same %{operations, capability} output.
Source.Workflow
Workflow sources expose a Jidoka.Workflow module as one operation.
| Field | Type | Purpose |
|---|---|---|
:workflow | module | Workflow module. Supports callback and DSL workflows. |
:name / :as | string or atom | Operation name the model sees. Defaults to workflow id. |
:description | string | Optional operation description override. |
:timeout | positive integer | Total wall-clock timeout in milliseconds. Defaults to 30_000. |
:forward_context | policy | :public, :none, {:only, keys}, or {:except, keys}. |
:result | :output or :structured | Raw workflow output or wrapped workflow metadata. |
:idempotency | operation idempotency | Defaults to :idempotent; :unsafe_once requires approval or controls. |
:metadata | map | Extra operation metadata merged into the operation. |
Published workflow operation metadata includes:
%{
"source" => "workflow",
"kind" => "workflow",
"workflow" => "refund_review",
"module" => "MyApp.Workflows.RefundReview",
"timeout" => 30_000,
"forward_context" => "{:only, [:tenant]}",
"result" => "structured",
"idempotency" => "idempotent",
"parameters_schema" => %{"type" => "object", ...}
}The capability validates the incoming Effect.OperationRequest, checks the
operation name, builds workflow context from forward_context and optional
arguments["context"], then calls Jidoka.Workflow.run/3. If the workflow
fails, the capability returns {:error, {:workflow_failed, operation_name, reason}}. If the operation source timeout expires, it returns
{:error, {:workflow_timeout, operation_name, timeout}}.
Common Patterns
- Compile once, reuse everywhere. Build the source struct at boot or in a
module attribute; call
Source.compile/1only when materializing a spec. - Combine sources by listing them.
Source.compile([local, mcp])returns one routed capability the runtime can call directly. - Keep capability functions pure-ish. Capabilities should be deterministic given the intent and journal; record any external state through the operation's output so the journal stays authoritative.
- Use
Effect.OperationRequest.from_input/1inside capabilities to decode the payload safely instead of pattern-matching the raw map.
Testing
A source test only needs the compile output and an Effect.Intent. No full
turn run is required.
test "local source executes its handler" do
{:ok, source} =
Jidoka.Operation.Source.Local.new(
operations: [
%{name: "echo", handler: fn args, _ctx -> {:ok, args} end}
]
)
{:ok, compiled} = Jidoka.Operation.Source.compile(source)
intent =
Jidoka.Effect.Intent.new(:operation,
%{name: "echo", arguments: %{"value" => 42}},
idempotency: :pure
)
assert {:ok, %{"value" => 42}} =
compiled.capability.(intent, Jidoka.Effect.Journal.new!(), Jidoka.Context.from_data!(%{}))
endTroubleshooting
| Symptom | Likely Cause | Fix |
|---|---|---|
{:error, {:duplicate_operation_source_name, name}} | Two sources publish the same operation. | Rename one operation or drop the duplicate source. |
{:error, {:missing_operation_handler, name}} | The compiled capability cannot route the operation. | Ensure the operation is published by a source compiled into the same plan. |
{:error, {:unsupported_effect_kind, kind}} | Capability was called with an :llm intent. | Operation capabilities only handle :operation intents; route LLM intents through Runtime.Capabilities.llm. |
Local source raises invalid_operation_handler | Handler is not 2- or 3-arity. | Use fn args, context -> ... end or fn intent, journal, context -> ... end. |
{:error, {:workflow_failed, name, reason}} | Workflow source ran but the workflow returned an error. | Inspect reason; DSL workflows include workflow id, step, kind, target, and cause on step failures. |
{:error, {:workflow_timeout, name, timeout}} | Workflow source exceeded its operation timeout. | Raise timeout: or move long work out of the synchronous workflow. |
Reference
Jidoka.Operation.Source- behaviour andcompile/2.Jidoka.Operation.Source.Local- in-process source.Jidoka.Operation.Source.MCP,Jidoka.Operation.Source.Subagent,Jidoka.Operation.Source.Handoff,Jidoka.Operation.Source.Workflow.Jidoka.Runtime.Capabilities- capability bundle consumed by the runtime.Jidoka.Effect.OperationRequest.
Related Guides
- Agent Spec Contract - where compiled operations live.
- Workflows - workflow DSL and workflow source options.
- Turn And Effect Contracts - the
Effect.Intentshape capabilities consume. - Runtime And Execution Layers - how the routed capability is invoked at run time.