The Jidoka DSL is small. It compiles agent modules to Jidoka.Agent.Spec.
Run compiled agents through Jidoka.chat/3 or Jidoka.turn/3. Agent modules
also generate bound convenience functions, but the root facade is the main
application path.
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
endUse approval: when a model-requested operation should pause before it runs:
tools do
action MyApp.RefundOrder,
idempotency: :unsafe_once,
approval: [
reason: :refund_requires_review,
message: "Review the refund before it is issued.",
ttl_ms: 300_000
]
endFor generated tool sources, approval can apply to every generated operation or only selected operation names:
tools do
mcp_tools endpoint: :github,
approval: [only: ["github_delete_repo"]]
catalog MyApp.AdminCatalog,
approval: :unsafe_once
endSupported approval values:
| Value | Meaning |
|---|---|
true | Require approval for every operation generated by this entry. |
false / omitted | No approval policy. |
:unsafe_once | Require approval only for generated operations marked :unsafe_once. |
| keyword/map | Optional only:, except:, when:, reason:, message:, ttl_ms:, and metadata:. |
Approval sugar compiles to the same durable review path as operation controls:
the turn hibernates, a Jidoka.Review.Request is exposed, and the application
resumes with approval or denial.
Use when: for a module-based Jidoka.ApprovalPredicate when approval depends
on operation arguments or caller context:
tools do
action MyApp.RefundOrder,
approval: [
when: MyApp.LargeRefundPredicate,
reason: :large_refund_review
]
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. Define the workflow in a module, then register it in
tools do.
defmodule MyApp.Workflows.RefundQuote do
use Jidoka.Workflow
workflow do
id :refund_quote
input Zoi.object(%{order_id: Zoi.string()})
end
steps do
function :quote, {MyApp.Refunds, :quote, 2},
input: %{order_id: input(:order_id)}
end
output from(:quote)
end
tools do
workflow MyApp.Workflows.RefundQuote,
as: :quote_refund,
timeout: 10_000,
forward_context: {:only, [:tenant]},
idempotency: :idempotent,
result: :structured
endWorkflow tool options:
| Option | Default | Purpose |
|---|---|---|
as: | workflow id | Operation name the model sees. |
description: | workflow description | Tool description. |
timeout: | 30_000 | Total workflow wall-clock timeout in milliseconds. |
forward_context: | :public | Runtime context forwarded into workflow execution. |
result: | :output | :output returns raw workflow output; :structured wraps workflow metadata. |
idempotency: | :idempotent | Operation safety level. Use :unsafe_once with approval or an operation control. |
approval: | omitted | Pause before the workflow operation executes. |
metadata: | %{} | Extra operation metadata. |
For workflow module authoring, refs, step kinds, direct execution, and testing, see Workflows.
subagent 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.
Use it when the parent should synthesize the child result and keep owning the
conversation.
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.
Use it when the next user message should be handled by a different agent.
tools do
handoff MyApp.BillingAgent,
as: :billing_specialist,
target: :auto,
forward_context: :public
endFor the decision model, context rules, and testing shape for both primitives, see Agent Orchestration.
Controls 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.