Use JSON or YAML when agents are authored outside Elixir. Jidoka imports the
document into the same Jidoka.Agent.Spec the DSL produces. Executable values
(action modules, control modules, Zoi schemas, Ash resources) resolve only
through caller-supplied registries, so imports never call String.to_atom/1 on
untrusted input.
When To Use This
- Use this guide when agents are authored outside Elixir (admin UI, config bundle, content repo).
- Use this guide when shipping agents as portable JSON/YAML that operations teams can edit.
- Do not use this guide when modules are in your code anyway; the DSL is shorter and gives you compile-time validation.
- Do not use this guide for arbitrary user-uploaded YAML without a trust story; the import boundary requires deliberate registries.
Prerequisites
- A working Jidoka project (see Getting Started).
- Familiarity with the operation contract from Tools And Operations.
- For YAML: the
:yaml_elixirdependency is already brought in by Jidoka. - A provider key in scope for the live
chat/3example. Tests can inject a fake LLM instead.
mix deps.get
mix test
Quick Example
The smallest portable agent is one YAML document plus an actions registry.
defmodule MyApp.Tools.LocalTime do
use Jidoka.Action,
name: "local_time",
description: "Returns the local time.",
schema: Zoi.object(%{city: Zoi.string() |> Zoi.default("Chicago")})
@impl true
def run(_params, _context), do: {:ok, %{city: "Chicago", time: "09:30"}}
end
yaml = """
version: 1
agent:
id: time_agent
model: openai:gpt-4o-mini
instructions: Call local_time when asked for the time.
tools:
actions:
- local_time
"""
{:ok, spec} =
Jidoka.import(yaml,
actions: %{"local_time" => MyApp.Tools.LocalTime}
)
{:ok, answer} = Jidoka.chat(spec, "What time is it in Chicago?")
answerThe same spec value comes back regardless of whether the agent was
authored in Elixir, JSON, or YAML.
Concepts
The import surface is three layers and one trust boundary.
Jidoka.Import.AgentDocumentis the on-the-wire shape, validated by a Zoi schema. The document has aversion(currently1), anagentblock, and optionaltools,controls,operations,runtime_defaults, andmetadatablocks.Jidoka.Importis the public compiler. It accepts a JSON or YAML string (or a decoded map), normalizes the document, resolves every non-data reference through caller-supplied registries, and produces aJidoka.Agent.Spec.- Registries are plain maps or keyword lists keyed by name. Five
registries are supported:
actions,ash_resources,controls,context_schemas,result_schemas. The trust boundary is here: only refs the caller put in the registry can become live modules or schemas.
╭───────────────────╮ ╭──────────────────────╮ ╭─────────────────╮
│ JSON / YAML │────▶│ Jidoka.Import.import │────▶│ AgentDocument │
│ (string or map) │ │ (format detected) │ │ (Zoi validated) │
╰───────────────────╯ ╰──────────┬───────────╯ ╰────────┬────────╯
│ │
▼ ▼
╭──────────────────────╮ ╭─────────────────╮
│ Registries │────▶│ Spec attrs │
│ - actions │ │ - operations │
│ - ash_resources │ │ - controls │
│ - controls │ │ - context_schema│
│ - context_schemas │ │ - result │
│ - result_schemas │ │ - memory etc. │
╰──────────────────────╯ ╰────────┬────────╯
│
▼
╭──────────────────╮
│ Jidoka.Agent.Spec│
│ (same as DSL) │
╰──────────────────╯Versioning
Every document carries version: 1. The schema validates the version and
returns {:error, {:unsupported_import_document_version, ...}} for
anything else. Treat the version as the only authoring contract guarantee;
new fields must be opt-in.
DSL/Import Parity
The DSL and the importer compile to the same Jidoka.Agent.Spec shape and
the same Jidoka.Agent.Spec.Operation entries. Golden DSL-to-spec tests catch
authoring drift early. When you add a feature to the DSL, add a matching key to
the document schema. When you add a key to the document, add a matching DSL
clause.
Trust Boundary
Imports never:
- call
String.to_atom/1orModule.concat/1on input; - load Elixir files from a path supplied by the document;
- assume a default registry; missing refs are an error.
Imports always:
- accept atoms or strings interchangeably in registry keys;
- resolve action modules, control modules, Ash resources, context schemas, and result schemas through the registries you pass;
- return
Jidoka.Error.Invalidon any unknown ref so failures are typed.
How To
Step 1: Pick A Format
Jidoka.import/2 detects JSON when the string starts with { or [, and
otherwise treats it as YAML. Force a format with format: :json or
format: :yaml when the heuristic is wrong.
{:ok, spec} = Jidoka.import(json_string, format: :json, actions: actions)
{:ok, spec} = Jidoka.import(yaml_string, format: :yaml, actions: actions)Step 2: Write The Document
The minimal document defines an agent block. Anything else is optional.
version: 1
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
memory:
scope: session
capture: conversation
max_entries: 8
tools:
actions:
- local_time
ash_resources:
- ref: account_resource
actions:
- read_account
browsers:
- name: docs
mode: read_only
allow:
- docs.example.com
controls:
max_turns: 8
timeout: 30000
inputs:
- control: no_secrets
operations:
- control: require_approval
when:
kind: action
name: local_time
outputs:
- control: safe_replyKey shapes match the DSL: tools.actions is a list of action refs;
controls.operations[].when is the same match map operation controls
accept in the DSL.
Step 3: Build The Registries
Each registry is a map (or keyword list) keyed by name. Values are real Elixir modules or Zoi schemas the caller already trusts.
registries = %{
actions: %{"local_time" => MyApp.Tools.LocalTime},
ash_resources: %{"account_resource" => MyApp.Accounts.User},
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()})}
}
{:ok, spec} = Jidoka.import(yaml, registries: registries)Jidoka.Import also accepts the registries as top-level options:
actions:, ash_resources:, controls:, context_schemas:,
result_schemas:. The forms are equivalent; pick one per project.
Step 4: Handle Missing Refs
A missing ref returns a typed validation error. Match on it explicitly when you accept user-authored documents.
case Jidoka.import(yaml, actions: %{}) do
{:ok, spec} ->
{:ok, spec}
{:error, %Jidoka.Error.Invalid{} = error} ->
%{details: %{reason: reason}} = Jidoka.error_to_map(error)
{:error, reason}
endFor the YAML above, that surfaces as
{:unknown_registry_ref, :actions, "local_time"} when the action registry
is empty.
Step 5: Use The Imported Spec Like Any Other
The result is Jidoka.Agent.Spec. Plan it, preflight it, run it, host it
under Jido - everything that works for a DSL agent works here.
{:ok, plan} = Jidoka.plan(spec)
{:ok, preflight} = Jidoka.preflight(spec, "ping")
{:ok, answer} = Jidoka.chat(spec, "ping")Step 6: Round-Trip With Inspect/Project
To compare authoring paths, lower both to inspection data:
imported = Jidoka.inspect(spec)
dsl = Jidoka.inspect(MyApp.SupportAgent)
imported.spec.operations == dsl.spec.operations
#=> true (when the document and DSL declare the same tools)This is exactly how the golden tests assert DSL/import parity.
Common Patterns
- Treat documents as data. Keep YAML alongside the application code, load it at boot, and pass the registries from a single trusted module.
- Use one registry map per environment. Production registries can include modules dev does not; the documents stay the same.
- Lean on the version field. Pin
version: 1; reject anything else rather than silently accepting unknown shapes. - Compose with
metadata. The document's free-formmetadatablock flows through toSpec.metadataand is visible fromJidoka.inspect/1- useful for owner tagging and feature flags. - Prefer atom and string interchangeability in registries.
the import registry matches
:fooagainst"foo"either direction so you can use whichever feels natural per environment.
Testing
A good import test exercises three behaviors: parsing, ref resolution, and parity with the equivalent DSL.
defmodule MyApp.ImportTest do
use ExUnit.Case, async: true
@yaml """
version: 1
agent:
id: time_agent
model: openai:gpt-4o-mini
instructions: Call local_time when asked for the time.
tools:
actions:
- local_time
"""
test "compiles a YAML document into a usable spec" do
{:ok, spec} =
Jidoka.import(@yaml,
actions: %{"local_time" => MyApp.Tools.LocalTime}
)
assert spec.id == "time_agent"
assert [%Jidoka.Agent.Spec.Operation{name: "local_time"}] = spec.operations
llm = fn _intent, journal ->
case map_size(journal.results) do
0 -> {:ok, %{type: :operation, name: "local_time", arguments: %{}}}
_ -> {:ok, %{type: :final, content: "09:30"}}
end
end
assert {:ok, "09:30"} = Jidoka.chat(spec, "now?", llm: llm)
end
test "missing action ref returns a typed validation error" do
assert {:error, %Jidoka.Error.Invalid{} = error} =
Jidoka.import(@yaml, actions: %{})
assert %{details: %{reason: {:unknown_registry_ref, :actions, "local_time"}}} =
Jidoka.error_to_map(error)
end
test "DSL and import compile to the same operations" do
{:ok, spec} =
Jidoka.import(@yaml,
actions: %{"local_time" => MyApp.Tools.LocalTime}
)
dsl_operations = MyApp.TimeAgent.spec().operations
assert Enum.map(spec.operations, & &1.name) ==
Enum.map(dsl_operations, & &1.name)
end
endTroubleshooting
| Symptom | Likely Cause | Fix |
|---|---|---|
{:error, %Jidoka.Error.Invalid{}} with reason {:unknown_registry_ref, ...} | A ref in the document was not in the matching registry. | Add the ref or remove it from the document; ref keys are case sensitive. |
{:error, {:unsupported_import_document_version, ...}} | The document version is not 1. | Bump or downgrade the document to match. |
{:error, {:unsupported_import_format, _}} | format: was set to something other than :json or :yaml. | Pass :json or :yaml, or remove the option to use detection. |
JSON decodes but agent is missing | The document was top-level keys without an agent: block. | Jidoka.Import normalizes well-known top-level agent keys, but unfamiliar ones are dropped. Wrap them in agent:. |
| Control fires for the wrong operation | The when: clause used a key that is not in the operation metadata. | Inspect with Jidoka.inspect(spec).spec.operations to confirm metadata keys; the DSL and importer use the same keys. |
Reference
Jidoka- public facade:Jidoka.import/2.Jidoka.Import- compiler and registry option handling:import/2,import!/2,load/2,load!/2.Jidoka.Import.AgentDocument- the validated document schema and version constant.- Import registry - registry fetch semantics used to resolve refs.
Jidoka.Agent.Spec- the spec shape both DSL and import compile into.Jidoka.Agent.Spec.Operation- the operation shape used for both authoring paths.
Related Guides
- Agent DSL - the Elixir-native authoring path and its parity with the importer.
- Tools And Operations - operation contract documents map to.
- Inspection And Preflight - comparing imported and DSL specs through inspection.
- Testing And Evals - using imported specs in deterministic eval cases.