squid-mesh-logo

Workflow automation runtime for Elixir apps.

CIHexHexDocsElixir ForumLicense: Apache 2.0

Squid Mesh provides a workflow DSL and runtime for Phoenix and OTP applications. It persists run, step, attempt, and audit state in Postgres and schedules execution through Oban. Workflows can model retries, waits, HITL approval gates, dependency joins, failure routes, replay, and inspection without running a separate workflow service.

Capabilities

Fit

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

Runtime Shape

Quick Start

Requirements:

1. Install from Hex.pm

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

If the host app defines custom steps with use Jido.Action, add :jido explicitly as well:

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

2. Configure Squid Mesh and Oban

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

config :my_app, Oban,
  repo: MyApp.Repo,
  queues: [squid_mesh: 10]

The host app's Oban config must include the :squid_mesh queue when Squid Mesh is using that queue name.

3. Install migrations

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

mix squid_mesh.install creates one current-schema Squid Mesh migration in the host app's priv/repo/migrations. The host app still owns its Oban setup and oban_jobs migration.

4. Import formatter rules

To keep workflow modules formatted as DSL-style calls, import Squid Mesh's formatter configuration from the host app:

# .formatter.exs
[
  import_deps: [:squid_mesh],
  inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

Example: Daily RSS To Discord

This example shows the core runtime shape: one cron trigger, typed payload defaults, built-in steps, custom steps, explicit failure routing, and step-level retry on the external side-effect step.

defmodule Content.Workflows.PostDailyDigest do
  use SquidMesh.Workflow

  workflow do
    trigger :daily_digest do
      cron "0 9 * * 1-5", timezone: "Etc/UTC"

      payload do
        field :feed_url, :string, default: "https://example.com/feed.xml"
        field :discord_webhook_url, :string
        field :posted_on, :string, default: {:today, :iso8601}
      end
    end

    step :fetch_feed, Content.Steps.FetchFeed, output: :feed
    
    step :build_digest, Content.Steps.BuildDigest,
      input: [:feed, :posted_on],
      output: :digest
    
    step :announce_post, :log, message: "Posting digest to Discord", level: :info
    step :record_failed_delivery, Content.Steps.RecordFailedDelivery

    step :post_to_discord, Content.Steps.PostToDiscord,
      input: [:digest, :discord_webhook_url],
      retry: [max_attempts: 5, backoff: [type: :exponential, min: 1_000, max: 30_000]]

    transition :fetch_feed, on: :ok, to: :build_digest
    transition :build_digest, on: :ok, to: :announce_post
    transition :announce_post, on: :ok, to: :post_to_discord
    transition :post_to_discord, on: :ok, to: :complete
    transition :post_to_discord, on: :error, to: :record_failed_delivery
    transition :record_failed_delivery, on: :ok, to: :complete
  end
end

Step modules implement domain work. Squid Mesh records durable state, schedules jobs through Oban, applies step retry policy, routes failures after retry exhaustion, and exposes run inspection.

For approval or manual-review gates, use approval_step/2 in transition-based workflows and resume the paused run through SquidMesh.approve_run/3 or SquidMesh.reject_run/3. Approval steps persist their resolved :ok and :error targets plus output-mapping metadata, so already-paused review runs keep the same decision semantics across restarts and deploys. Generic SquidMesh.unblock_run/2 remains available for lower-level :pause steps when you need manual intervention without an explicit approve/reject contract.

When a step needs a narrower contract than the whole payload plus accumulated context, use input: [...] to select keys and output: :key to namespace the returned map for downstream steps.

For external side effects that cannot be honestly undone, mark the step with irreversible: true or compensatable: false. Squid Mesh exposes that recovery policy in inspection and blocks replay by default after such a step completes; operators can still replay with allow_irreversible: true after reviewing the side effect.

Start the workflow through the public API and inspect the result with history:

{:ok, run} =
  SquidMesh.start_run(Content.Workflows.PostDailyDigest, %{
    discord_webhook_url: webhook_url
  })

SquidMesh.inspect_run(run.id, include_history: true)

With history enabled, the inspected run includes chronological step_runs, declared steps state, and durable audit_events for pause, resume, approval, and rejection actions.

Use SquidMesh.explain_run/2 when a host app needs operator-facing diagnostics:

{:ok, explanation} = SquidMesh.explain_run(run.id)

explanation.reason
#=> :waiting_for_retry

inspect_run/2 returns the persisted runtime facts. explain_run/2 summarizes the current reason, valid next actions, and evidence in a structured shape that dashboards and CLIs can render themselves.

Documentation

Use the docs index for setup, workflow authoring, operations, and architecture:

Contributing