README.md

<div align="center">

  <img width="300" alt="squid-mesh-logo" src="https://github.com/user-attachments/assets/274ff13a-25d4-4d94-84ff-637176c91d4e" />

  <p><strong>Workflow automation runtime for Elixir apps.</strong></p>
  
  <p>
    <a href="https://github.com/ccarvalho-eng/squid_mesh/actions/workflows/ci.yml">
      <img alt="CI" src="https://github.com/ccarvalho-eng/squid_mesh/actions/workflows/ci.yml/badge.svg" />
    </a>
    <a href="https://hex.pm/packages/squid_mesh">
      <img alt="Hex" src="https://img.shields.io/hexpm/v/squid_mesh" />
    </a>
    <a href="https://hexdocs.pm/squid_mesh">
      <img alt="HexDocs" src="https://img.shields.io/badge/docs-hexdocs-purple" />
    </a>
    <a href="https://elixirforum.com/t/squid-mesh-workflow-automation-runtime-for-elixir-applications/75162">
      <img alt="Elixir Forum" src="https://img.shields.io/badge/Elixir_Forum-Join_Discussion-4B275F?logo=elixir&logoColor=white" />
    </a>
    <a href="https://github.com/ccarvalho-eng/squid_mesh/blob/main/LICENSE">
      <img alt="License: Apache 2.0" src="https://img.shields.io/badge/license-Apache%202.0-blue.svg" />
    </a>
  </p>
</div>

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

- workflow DSL with manual and cron triggers
- Postgres-backed run, step, attempt, and audit history
- Oban-based step execution, delayed scheduling, redelivery, and retries
- retries, waits, failure routes, dependency joins, and HITL approval gates
- explicit step input selection and output mapping
- same-process host repo transactions for small local step groups
- runtime inspection through declared step state, audit events, and `SquidMesh.explain_run/2`
- built-in steps like `:log`, `:wait`, `:pause`, and `:approval`, plus custom steps with `Jido.Action`

## Fit

- workflow state should survive app restarts, deploys, retries, and Oban redelivery
- a Phoenix context needs durable approval, recovery, notification, or back-office flow state
- step history and manual decisions need to be inspectable after execution
- workflow state belongs in the host app's Postgres database, not a separate service

> [!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](docs/production_readiness.md) for the current checklist and remaining bar.

## Runtime Shape

- Squid Mesh owns workflow structure, payload validation, runtime state, and retry policy
- Oban owns durable execution, queueing, delayed scheduling, and redelivery
- your host app keeps its existing `Repo`, `Oban`, and application boundaries

## Quick Start

Requirements:

- an existing Elixir application
- an existing Ecto `Repo`
- Postgres for persisted runtime state
- an existing `Oban` setup for background execution

### 1. Install from Hex.pm

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

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

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

### 2. Configure Squid Mesh and Oban

```elixir
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

```sh
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:

```elixir
# .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, step-level
retry on the external side-effect step, and compensation for a successfully
posted Discord message if a later bookkeeping step fails.

```elixir
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_successful_delivery, Content.Steps.RecordSuccessfulDelivery,
      input: [:discord_message, :posted_on]

    step :record_failed_delivery, Content.Steps.RecordFailedDelivery

    step :post_to_discord, Content.Steps.PostToDiscord,
      input: [:digest, :discord_webhook_url],
      output: :discord_message,
      compensate: Content.Steps.DeleteDiscordMessage,
      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: :record_successful_delivery
    transition :post_to_discord, on: :error, to: :record_failed_delivery
    transition :record_successful_delivery, on: :ok, to: :complete
    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.

When a custom step needs several local repo writes to commit or roll back
together, declare `transaction: :repo`. This wraps only that action callback in
the configured Ecto repo transaction; workflow durability, successor dispatch,
external side effects, and saga compensation remain explicit Squid Mesh
boundaries.

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.

In the RSS example, the `:error` transition on `:post_to_discord` is a
same-step fallback for a message that was never posted successfully after
retries. The compensation callback is different: it is used only if
`:post_to_discord` completes, stores a deletable Discord message id under
`:discord_message`, and a later step such as `:record_successful_delivery`
causes the run to fail.

For other reversible saga steps, declare compensation callbacks the same way:

```elixir
step :reserve_inventory, Checkout.Steps.ReserveInventory,
  compensate: Checkout.Steps.ReleaseInventory

step :authorize_payment, Checkout.Steps.AuthorizePayment,
  compensate: Checkout.Steps.VoidPaymentAuthorization

step :capture_payment, Checkout.Steps.CapturePayment,
  retry: [max_attempts: 2]

transition :reserve_inventory, on: :ok, to: :authorize_payment
transition :authorize_payment, on: :ok, to: :capture_payment
transition :capture_payment, on: :ok, to: :complete
```

When a downstream step fails after retries and the workflow has no forward
`:error` path, Squid Mesh runs completed compensation callbacks in reverse
completion order. In the checkout example above, a failed `:capture_payment`
step voids the payment authorization before releasing inventory, and each
result is persisted under the original step's `recovery.compensation` history.

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

```elixir
{: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:

```elixir
{: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:

- [Docs index](docs/index.md)
- [Host app integration](docs/host_app_integration.md)
- [Workflow authoring guide](docs/workflow_authoring.md)
- [Example host app](https://github.com/ccarvalho-eng/squid_mesh/tree/main/examples/minimal_host_app)

## Contributing

- [Contributing guide](CONTRIBUTING.md)
- [Code of conduct](CODE_OF_CONDUCT.md)