Jido Action

Hex.pmHex DocsCILicenseWebsiteEcosystemDiscord

Validated Actions and data-first Flow composition for Elixir.

jido_action is part of the Jido ecosystem. See jido.run for the project and its packages.

jido_action defines validated actions, executable call frames, data-first Flows, and one public execution boundary.

Jido Flow is a declarative, in-memory graph execution layer for Jido Actions. Runic owns graph mechanics, planning, runnable discovery, node execution, and graph-state transitions. Jido Flow owns its DSL, validation, lossless Map/JSON representation, compilation, and Flow semantics. Jido Exec owns one in-memory execution session: step-wise execution, bounded concurrency, Action invocation, errors, telemetry, and final results.

Durable orchestration is not provided. An outer system must own persistence, queues, scheduling, recovery, retries, cancellation policy, distributed coordination, supervision, and deployment-safe continuation. Jido.Exec can enforce one caller-selected timeout for a complete in-memory call.

This foundation keeps the action boundary small:

Version 3.0.0-beta.1 is a public beta. It introduces the declarative Flow DSL, runtime Flow construction, safe stored Flow maps, and one Flow execution engine. The v3 API can still change before the stable release, and this beta locks Runic 0.1.0-alpha.9. Use it for evaluation and controlled trials before you use it for critical production work. See the v3 migration guide for the confirmed breaking changes.

Install

def deps do
[
{:jido_action, "~> 3.0.0-beta.1"}
]
end

Define An Action

defmodule MyApp.Actions.GreetUser do
use Jido.Action,
name: "greet_user",
description: "Builds a greeting for a user",
schema:
Zoi.object(%{
name: Zoi.string() |> Zoi.min(1),
excited?: Zoi.boolean() |> Zoi.default(false)
}),
output_schema:
Zoi.object(%{
greeting: Zoi.string()
})
@impl true
def run(%{name: name, excited?: excited?}, _context) do
suffix = if excited?, do: "!", else: "."
{:ok, %{greeting: "Hello, #{name}#{suffix}"}}
end
end

Public action functions:

Run An Action

{:ok, %{greeting: "Hello, Ada!"}} =
Jido.Exec.run(
MyApp.Actions.GreetUser,
%{name: "Ada", excited?: true},
%{request_id: "req-123"}
)

Jido.Exec validates the Action input and output and runs the Action under the configured Task Supervisor. Code that integrates its own executor can use validate_params/1, run/2, and validate_output/1 directly.

The Action run/2 callback must return one of:

Three-tuple returns let callers receive an extra value alongside the result or error.

Capture A Call Frame

Use Jido.Instruction when the intent to run an executable needs to be passed, logged, queued, or enriched before execution.

instruction =
Jido.Instruction.new!(
target: MyApp.Actions.GreetUser,
params: %{name: "Ada"},
context: %{request_id: "req-123"}
)

An Instruction holds one Action module, Flow module, or runtime Flow target. It does not define a workflow, program, or runtime policy.

Compose A Flow

Use Jido.Flow when several actions must execute as one validated graph.

defmodule MyApp.Actions.Notify do
use Jido.Action,
name: "notify",
schema: Zoi.object(%{message: Zoi.string()})
@impl true
def run(%{message: message}, _context) do
{:ok, %{message: message, status: "queued"}}
end
end
defmodule MyApp.Flows.GreetAndNotify do
use Jido.Flow,
name: "greet_and_notify",
schema: Zoi.object(%{name: Zoi.string()}),
output_schema: Zoi.map()
flow do
step "greet",
action: MyApp.Actions.GreetUser,
params: %{name: input(:name), excited?: false}
step "notify",
action: MyApp.Actions.Notify,
params: %{message: select(result("greet"), :greeting)}
output result("notify")
end
end
{:ok, result} =
Jido.Exec.run(MyApp.Flows.GreetAndNotify, %{name: "Ada"}, %{})

Every Flow declares one output expression. Flows also support ordered Choices, Map and Reduce collections, bounded Iterate components with State, independent components that can run in parallel, and a step-wise execution API.

Build A Flow At Runtime

Use Jido.Flow.Builder when runtime data defines the graph. Each node has an explicit name, and each result reference uses that name.

alias Jido.Flow.Builder
builder =
Builder.new(name: "runtime_greeting")
|> Builder.step(
"greet",
MyApp.Actions.GreetUser,
%{name: Builder.input(:name), excited?: Builder.value(false)}
)
|> Builder.output(Builder.result("greet"))
{:ok, runtime_flow} = Builder.build(builder)
{:ok, %{greeting: "Hello, Ada."}} =
Jido.Exec.run(runtime_flow, %{name: "Ada"})

The Builder and the Flow module DSL produce the same canonical Flow model.

Load A Flow From JSON Or A Map

Use a versioned stored map when a database, web UI, or AI system defines the Flow. The host owns a flat Jido.Flow.Registry that maps stable identifiers to trusted Action modules, schemas, and data atoms.

registry =
Jido.Flow.Registry.new!(%{
"actions/greet-user/v1" => {:action, MyApp.Actions.GreetUser},
"schemas/empty/v1" => {:schema, []},
"atoms/excited/v1" => {:atom, :excited?},
"atoms/name/v1" => {:atom, :name}
})
{:ok, stored} = Jido.Flow.Codec.encode(runtime_flow, registry)
json = JSON.encode!(stored)
decoded = JSON.decode!(json)
case Jido.Flow.Codec.decode(decoded, registry) do
{:ok, flow} ->
Jido.Flow.validate_executable(flow)
{:error, error} ->
{:error, Jido.Flow.Error.to_map(error)}
end

Jido.Flow.Codec.decode/2 does not execute the Flow. Invalid or incomplete maps return a structured error instead of raising. Stored identifiers cannot create atoms or select a module outside the host Registry.

Use Jido.Flow.Codec.diagnose/2 for a browser or AI editor that needs all independent stored-document and graph errors. It returns one ordered Splode error group with JSON paths and never returns a partial Flow.

The Flow module DSL, Builder, stored JSON Codec, and direct constructors produce one canonical %Jido.Flow{} model. The Codec uses explicit component kinds. It does not infer old records or module names.

Run A Flow Step By Step

Run-to-completion and step-wise execution use the same engine:

{:ok, execution} = Jido.Exec.start(runtime_flow, %{name: "Ada"})
[runnable] = Jido.Exec.ready(execution)
%Runic.Workflow{} = Jido.Exec.workflow(execution)
%Jido.Flow.Compiled{} = Jido.Exec.compiled(execution)
{:ok, %Runic.Workflow.Runnable{status: :completed}, execution} =
Jido.Exec.step(execution, runnable)
:succeeded = Jido.Exec.status(execution)
{:ok, %{greeting: "Hello, Ada."}} = Jido.Exec.result(execution)

wave/1 runs the current ready set. continue/1 runs until the Flow reaches a terminal result. Always pass the newest execution value to the next call. The caller owns this in-memory lifecycle. Jido does not persist or recover it. Jido rejects reuse of a stale execution revision.

Observe Execution

Telemetry covers Action, Flow, Flow node, and collection work-unit lifecycles. Direct Actions and Instructions emit [:jido, :action, :start], [:jido, :action, :stop], and [:jido, :action, :error]. Flows and their nodes use the [:jido, :flow] namespace. Map items, Reduce items, and Iterate iterations add work-unit spans in that namespace. One execution_id correlates nested work. Step and selected Choice Actions emit a target lifecycle with the Action module and selected option. An Action inside a Flow does not emit a separate direct Action lifecycle. Telemetry observes execution only; it does not control scheduling or results.

See Execution for exact event names, measurements, metadata, nesting, and step-wise semantics.

Docs

Start with the runnable Getting Started Livebook. ExDoc adds a Run in Livebook link to each .livemd guide.

Start Here

Core Contracts

Author Flows

Run And Operate

Upgrade

Jido Ecosystem

Contributing

See the contribution guide for development and pull-request guidance.

License

Copyright 2024-2026 Mike Hostetler

Licensed under the Apache License, Version 2.0. See LICENSE.