logo

Workflow automation platform for Elixir applications.

CIHexHexDocsLicense: Apache 2.0

Squid Mesh lets Phoenix and OTP applications define, run, inspect, replay, and recover durable workflows in code.

[!WARNING] Squid Mesh is still in early development. The runtime is suitable for evaluation, local development, and integration work, but it is not yet positioned as production-ready. See Production Readiness for the current checklist and remaining bar.

Requirements

Supported Baseline

Current verified baseline:

Component Baseline
Elixir 1.19.5-otp-28
Erlang/OTP 28.4.1
Postgres 15+
Oban 2.21 and 2.22
Jido 2.0+

See Compatibility Matrix for support policy details.

Installation

Add Squid Mesh to your application's dependencies:

defp deps do
  [
    {:squid_mesh, "~> 0.1.0-alpha.1"}
  ]
end

If you want to evaluate directly from Git instead of Hex:

defp deps do
  [
    {:squid_mesh, github: "ccarvalho-eng/squid_mesh", tag: "v0.1.0-alpha.1"}
  ]
end

Install Squid Mesh's library-owned migrations into the host application and run them through the host app's normal migration flow:

mix deps.get
mix squid_mesh.install
mix ecto.migrate

mix squid_mesh.install copies only Squid Mesh tables into priv/repo/migrations. It does not manage oban_jobs; embedded applications are expected to use their own existing Oban setup.

If you are wiring Squid Mesh into a fresh app rather than an existing one, add an Oban migration first and run it through the host app's normal migration flow:

defmodule MyApp.Repo.Migrations.AddObanJobs do
  use Ecto.Migration

  def up, do: Oban.Migrations.up()
  def down, do: Oban.Migrations.down()
end

Configuration

Configure Squid Mesh under the :squid_mesh application:

config :squid_mesh,
  repo: MyApp.Repo,
  execution: [
    name: Oban,
    queue: :squid_mesh
  ]

Public runtime API:

First successful run checklist:

  1. Add the :squid_mesh dependency.
  2. Make sure the host app already owns a working Repo.
  3. Make sure the host app already owns a working Oban instance and oban_jobs table.
  4. Run mix squid_mesh.install.
  5. Run mix ecto.migrate.
  6. Configure :squid_mesh with the host app's Repo and Oban queue.
  7. Start the host app's Repo and Oban under supervision.
  8. Start one workflow through SquidMesh.start_run/2 or start_run/3.

For a standalone development harness with its own Repo and Oban, use examples/minimal_host_app.

To activate cron triggers in a host app, opt in through the host app's Oban plugins:

config :my_app, Oban,
  repo: MyApp.Repo,
  plugins: [
    {SquidMesh.Plugins.Cron,
     workflows: [
       MyApp.Workflows.DailyStandup
     ]}
  ],
  queues: [squid_mesh: 10]

SquidMesh.Plugins.Cron uses Oban's cron scheduler underneath. Squid Mesh does not run a separate scheduler or manage oban_jobs itself.

Runtime Overview

Jido's role today is intentionally narrow:

Likely future direction:

C4 Container View

+--------------------------------------------------------------------------------+
| Person / System: Host Elixir or Phoenix application                            |
|                                                                                |
| - defines workflow modules with `use SquidMesh.Workflow`                       |
| - starts and inspects runs through `SquidMesh`                                 |
| - may opt into `SquidMesh.Plugins.Cron` through its Oban config                |
+---------------------------------------------+----------------------------------+
                                              |
                                              v
+--------------------------------------------------------------------------------+
| Container: Squid Mesh library                                                  |
|                                                                                |
| Responsibilities                                                               |
| - public runtime API (`SquidMesh`)                                             |
| - workflow definition + payload/trigger validation                             |
| - durable run state (`RunStore`, `StepRunStore`, `AttemptStore`)               |
| - step execution flow (`Dispatcher` -> `StepWorker` -> `StepExecutor`)         |
| - retry policy, replay/cancel semantics, built-in steps, observability         |
+-------------------------------+-----------------------------+------------------+
                                |                             |
                                v                             v
                    +-----------+-----------+     +-----------+-----------+
                    | Container: Oban       |     | Container: Jido       |
                    | durable job execution |     | executes custom step  |
                    | queues + scheduling   |     | modules               |
                    +-----------+-----------+     +-----------+-----------+
                                |                             ^
                                v                             |
                    +-----------+-----------------------------+-----------+
                    | Container: Postgres                                 |
                    | source of truth for runs, step runs, attempts,      |
                    | and Oban-managed jobs                               |
                    +-----------------------------------------------------+

External integration path:
custom workflow step -> `SquidMesh.Tools` -> adapter (for example HTTP) -> external system

Execution Model

Squid Mesh does not try to re-implement worker coordination that Oban already provides. The runtime stays focused on workflow semantics and durable run state, while Oban remains the execution engine underneath.

Recovery Boundaries

V1 guarantees:

V1 does not claim:

If a workflow step talks to an external system, the step should own its own idempotency key or duplicate-protection strategy at that boundary.

Workflow Example

defmodule Billing.Workflows.PaymentRecovery do
  use SquidMesh.Workflow

  workflow do
    trigger :payment_recovery do
      manual()

      payload do
        field(:account_id, :string)
        field(:invoice_id, :string)
        field(:attempt_id, :string)
        field(:gateway_url, :string)
      end
    end

    step(:load_invoice, Billing.Steps.LoadInvoice)
    step(:wait_for_settlement, :wait, duration: 5_000)
    step(:log_recovery_attempt, :log,
      message: "Invoice loaded, checking gateway status",
      level: :info
    )
    step(:check_gateway_status, Billing.Steps.CheckGatewayStatus,
      retry: [max_attempts: 5, backoff: [type: :exponential, min: 1_000, max: 30_000]]
    )
    step(:notify_customer, Billing.Steps.NotifyCustomer)

    transition(:load_invoice, on: :ok, to: :wait_for_settlement)
    transition(:wait_for_settlement, on: :ok, to: :log_recovery_attempt)
    transition(:log_recovery_attempt, on: :ok, to: :check_gateway_status)
    transition(:check_gateway_status, on: :ok, to: :notify_customer)
    transition(:notify_customer, on: :ok, to: :complete)
  end
end

Step Example

defmodule Billing.Steps.CheckGatewayStatus do
  use Jido.Action,
    name: "check_gateway_status",
    description: "Checks gateway state",
    schema: [
      invoice: [type: :map, required: true],
      gateway_url: [type: :string, required: true]
    ]

  @impl true
  def run(%{invoice: invoice, gateway_url: gateway_url}, _context) do
    case SquidMesh.Tools.invoke(SquidMesh.Tools.HTTP, %{
           method: :get,
           url: gateway_url,
           timeout: 1_000
         }) do
      {:ok, result} ->
        {:ok,
         %{
           gateway_check: %{
             status: result.payload.body,
             invoice_id: invoice.id,
             status_code: result.payload.status
           }
         }}

      {:error, error} ->
        {:error, SquidMesh.Tools.Error.to_map(error)}
    end
  end
end

Call It From Your App

defmodule Billing.WorkflowRuns do
  def start_payment_recovery(account_id, invoice_id, attempt_id) do
    SquidMesh.start_run(Billing.Workflows.PaymentRecovery, :payment_recovery, %{
      account_id: account_id,
      invoice_id: invoice_id,
      attempt_id: attempt_id,
      gateway_url: "https://gateway.internal/checks/#{attempt_id}"
    })
  end

  def inspect_payment_recovery(run_id) do
    SquidMesh.inspect_run(run_id)
  end

  def list_recent_payment_recoveries do
    SquidMesh.list_runs(workflow: Billing.Workflows.PaymentRecovery, limit: 20)
  end

  def cancel_payment_recovery(run_id) do
    SquidMesh.cancel_run(run_id)
  end

  def replay_payment_recovery(run_id) do
    SquidMesh.replay_run(run_id)
  end
end

If a workflow defines a single trigger, SquidMesh.start_run/2 remains the short path and uses that default trigger automatically.

To inspect the run with step and attempt history:

SquidMesh.inspect_run(run_id, include_history: true)

Trigger boundary today:

Workflows can mix custom step modules and built-in primitives:

Workflow steps can also call tool adapters through the shared boundary:

Retry behavior stays on the step that owns the work:

step(:check_gateway_status, Billing.Steps.CheckGatewayStatus,
  retry: [max_attempts: 5, backoff: [type: :exponential, min: 1_000, max: 30_000]]
)

That keeps retry policy on the step that owns the work while Squid Mesh and Oban handle delayed rescheduling underneath.

Run lifecycle states currently include:

Documentation

Contributing

Fast local smoke path:

cd examples/minimal_host_app
MIX_ENV=test mix example.smoke