The Jidoka DSL is small. It compiles agent modules to Jidoka.Agent.Spec.
Minimal Agent
defmodule MyApp.Assistant do
use Jidoka.Agent
agent :assistant
endThis uses default instructions and the configured default model.
Agent Block
agent :support_agent do
model "openai:gpt-4o-mini"
instructions "Answer support questions tersely."
context Zoi.object(%{tenant_id: Zoi.string()})
result schema: Zoi.object(%{answer: Zoi.string()}), max_repairs: 1
memory scope: :session, max_entries: 5
endSupported fields:
model- any ReqLLM/LLMDB model input, such as"openai:gpt-4o-mini"or%{provider: :openai, id: "gpt-4o-mini"};generation- optional provider-facing generation overrides; omitted agents useJidoka.Config.default_generation/0;instructions- system-level behavior instructions;context- optional Zoi schema for runtime context validation;result- optional Zoi schema orJidoka.Agent.Spec.Resultdata for structured app-facing turn results;memory- optional memory policy data, ortruefor defaults.
When result is declared, models still return final assistant text, but the
runtime also validates the final structured value and exposes it as
Jidoka.Turn.Result.value. Invalid values trigger a bounded repair loop using
max_repairs; after the bound is exhausted, the turn fails with a result-phase
error.
Tools Block
tools do
action MyApp.LocalTime
ash_resource MyApp.Accounts.Customer, actions: [:read, :create]
browser :docs, allow: ["https://docs.example.com"]
mcp_tools endpoint: :support_mcp, prefix: "support_"
skill MyApp.Skills.RefundPolicy
load_path "priv/skills"
workflow MyApp.Workflows.RefundQuote, as: :quote_refund
subagent MyApp.EvidenceAgent, as: :collect_evidence
handoff MyApp.BillingAgent, as: :billing_specialist
endThe tools block is authoring vocabulary for anything the model can ask
Jidoka to do. Every entry compiles to one or more Agent.Spec.Operation
records. The runtime still sees a small operation boundary; the DSL gives you
clear source-specific syntax.
Supported entries:
| DSL entry | Use it for | Runtime shape |
|---|---|---|
action MyApp.Action | One deterministic Jido/Jidoka action. | One :action operation. |
ash_resource MyApp.Resource | AshJido-generated actions from an Ash resource. | One operation per exposed Ash action. |
browser :docs | Search/read/snapshot browser capabilities backed by jido_browser. | :browser operations. |
mcp_tools endpoint: :id | Tools exposed by a configured MCP endpoint. | One :mcp operation per discovered/static tool. |
skill MyApp.Skill | Jido.AI skill instructions and any declared operations. | Skill prompt plus :skill operations when present. |
load_path "priv/skills" | Runtime-loaded SKILL.md files. | Adds skill instructions from the path. |
workflow MyApp.Workflow | Deterministic application workflow as one callable operation. | One :workflow operation. |
subagent MyApp.Agent | Bounded delegation to another Jidoka agent for one task. | One :subagent operation. |
handoff MyApp.Agent | Transfer future conversation ownership to another agent. | One :handoff operation. |
action is the smallest tool source. The module must be a Jido action or a
compatible module exposing to_tool/0.
tools do
action MyApp.LocalTime
endash_resource records an Ash resource source. AshJido-generated actions are
imported as :ash_resource operations. Use actions: to filter the generated
operations exposed to the model:
tools do
ash_resource MyApp.Accounts.User, actions: [:read, :create]
endbrowser expands to constrained browser operations backed by Jido action
wrappers for the jido_browser read-only tools: search_web, read_page, and
snapshot_url in :read_only mode.
tools do
browser :docs, mode: :read_only, allow: ["https://hexdocs.pm"]
endmcp_tools imports tools from a configured MCP endpoint. Use prefix: to keep
remote tool names distinct from local operation names.
tools do
mcp_tools endpoint: :support_mcp,
prefix: "support_",
tools: [%{name: "lookup_policy", description: "Look up support policy."}]
endskill and load_path add Jido.AI skill context. A skill can add prompt
instructions, operation metadata, or both depending on the skill definition.
tools do
skill MyApp.Skills.RefundPolicy
load_path "priv/skills"
endworkflow exposes deterministic application code as one operation. Use it when
the model should choose a business workflow, but your application owns the
workflow steps.
tools do
workflow MyApp.Workflows.RefundQuote,
as: :quote_refund,
timeout: 10_000,
result: :structured
endsubagent delegates one bounded task to another Jidoka agent and returns the
child result to the parent. It does not change who owns the next user turn.
tools do
subagent MyApp.EvidenceAgent,
as: :collect_evidence,
timeout: 30_000,
result: :structured
endhandoff records that another agent should own future turns for a conversation.
The current turn still completes normally; your application reads
Jidoka.handoff/1 to route the next message.
tools do
handoff MyApp.BillingAgent,
as: :billing_specialist,
target: :auto,
forward_context: :public
endControls Block
Controls describe policy at explicit turn boundaries:
inputruns before the first model call;operationdescribes policy around model-callable work;outputruns before the final answer is returned;max_turnsandtimeoutbound the turn loop.
defmodule MyApp.NoSecrets do
use Jidoka.Control, name: "no_secrets"
@impl true
def call(%{input: input}) do
if String.contains?(input, "secret"), do: {:block, :secret_input}, else: :cont
end
end
defmodule MyApp.RequireApproval do
use Jidoka.Control, name: "require_approval"
@impl true
def call(%Jidoka.Runtime.Controls.OperationContext{} = operation) do
if operation.operation == "local_time" do
{:interrupt, :approval_required}
else
:cont
end
end
end
defmodule MyApp.SafeReply do
use Jidoka.Control, name: "safe_reply"
@impl true
def call(_result), do: :cont
end
controls do
max_turns 8
timeout 30_000
input MyApp.NoSecrets
operation MyApp.RequireApproval,
when: [kind: :action, name: :local_time]
output MyApp.SafeReply
endInput, operation, and output controls run in declaration order. Operation controls receive
Jidoka.Runtime.Controls.OperationContext data and may return :cont,
{:block, reason}, {:interrupt, reason}, or {:error, reason}.
An operation interrupt hibernates the turn with a :review cursor and a
Jidoka.Review.Request in snapshot.metadata["pending_review"].
See Controls for approval, limit, and testing examples.
Compiled Shape
The important boundary is the compiled spec:
%Jidoka.Agent.Spec{
id: "support_agent",
model: %LLMDB.Model{},
generation: %Jidoka.Agent.Spec.Generation{},
context_schema: %Zoi.Schema{},
result: %Jidoka.Agent.Spec.Result{},
operations: [%Jidoka.Agent.Spec.Operation{}],
controls: %Jidoka.Agent.Spec.Controls{}
}Golden tests in test/jidoka/golden/dsl_to_spec_test.exs lock the projected
shape.
Import Parity
JSON/YAML imports compile into the same Jidoka.Agent.Spec shape. Portable
documents stay data-only, so action modules and Zoi context schemas are named in
the document and resolved with registries. Import currently covers the data-safe
agent fields, controls, and the portable tool sources: action,
ash_resource, browser, and mcp_tools.
agent:
id: support_agent
model: openai:gpt-4o-mini
instructions: Answer support questions tersely.
context:
ref: support_context
result:
ref: support_result
max_repairs: 1
tools:
actions:
- local_time
ash_resources:
- ref: account_resource
actions:
- read_account
browsers:
- name: docs
mode: search
allow:
- docs.example.com
mcp_tools:
- endpoint: support_mcp
prefix: support_
tools:
- name: lookup_policy
description: Look up support policy.
controls:
max_turns: 8
timeout: 30000
inputs:
- control: no_secrets
operations:
- control: require_approval
when:
kind: action
name: local_time
outputs:
- control: safe_replyyaml = """
agent:
id: support_agent
model: openai:gpt-4o-mini
instructions: Answer support questions tersely.
context:
ref: support_context
result:
ref: support_result
max_repairs: 1
tools:
actions:
- local_time
browsers:
- name: docs
mode: search
controls:
max_turns: 8
timeout: 30000
inputs:
- control: no_secrets
operations:
- control: require_approval
when:
kind: action
name: local_time
outputs:
- control: safe_reply
"""
{:ok, spec} =
Jidoka.import(yaml,
registries: %{
actions: %{"local_time" => MyApp.LocalTime},
ash_resources: %{"account_resource" => MyApp.AccountResource},
controls: %{
"no_secrets" => MyApp.NoSecrets,
"require_approval" => MyApp.RequireApproval,
"safe_reply" => MyApp.SafeReply
},
context_schemas: %{"support_context" => Zoi.object(%{tenant_id: Zoi.string()})},
result_schemas: %{"support_result" => Zoi.object(%{answer: Zoi.string()})}
}
)String refs are resolved only through explicit registries; imports do not create atoms or modules from untrusted input.
Not In The DSL Yet
The current DSL does not expose session queues, approval queues, or native provider tool-calling. Runtime additions remain explicit Elixir code, not agent DSL.