# Getting Started With Squidie

This Livebook introduces the core Squidie model through a small workflow you
can run without creating a Phoenix app or a Postgres database.

The examples use ETS-backed journal storage to keep setup light. Real host apps
should use the Ecto/Postgres journal storage described in the host app
integration guide.

## Install

```elixir
Mix.install([
  {:squidie, git: "https://github.com/dark-trench/squidie.git"}
])
```

## Runtime Setup

Squidie normally reads runtime configuration from the host application. In
this notebook, we configure it directly and use an ETS table for durable facts
inside the current Livebook session.

```elixir
storage = {Jido.Storage.ETS, table: :squidie_getting_started_livebook}

# This placeholder satisfies the library config contract for the notebook.
# A real host app should configure its actual Ecto repo instead.
defmodule SquidieLivebook.Repo do
end

Application.put_env(:squidie, :repo, SquidieLivebook.Repo)
Application.put_env(:squidie, :runtime, :journal)
Application.put_env(:squidie, :read_model, :read_model)
Application.put_env(:squidie, :journal_storage, storage)
Application.put_env(:squidie, :queue, "livebook")

opts = [
  runtime: :journal,
  journal_storage: storage,
  queue: "livebook"
]

defmodule SquidieLivebook.Output do
  def attempt(attempt) do
    Map.take(attempt, [
      :step,
      :status,
      :attempt_number,
      :visible_at,
      :wakeup_emitted?,
      :applied?
    ])
  end

  def runnable(runnable) do
    Map.take(runnable, [:runnable_key, :key, :step, :status, :visible_at])
  end

  def node(node), do: Map.take(node, [:id, :status, :current?])

  def edge(edge) do
    Map.take(edge, [:id, :from, :to, :type, :status, :selected?, :pending?])
  end
end
```

## Define Steps

Workflow steps do the domain work. The common authoring path is
`use Squidie.Step`; raw `Jido.Action` modules are only needed for explicit
interop.

```elixir
defmodule SquidieLivebook.PackLembas do
  use Squidie.Step,
    name: :pack_lembas,
    description: "Packs provisions for the errand",
    input_schema: [
      ring_id: [type: :string, required: true]
    ],
    output_schema: [
      provisions: [type: :map, required: true]
    ]

  @impl Squidie.Step
  def run(%{ring_id: ring_id}, %Squidie.Step.Context{}) do
    {:ok,
     %{
       provisions: %{
         ring_id: ring_id,
         lembas_count: 11,
         packed_by: "Sam"
       }
     }}
  end
end

defmodule SquidieLivebook.CrossMoria do
  use Squidie.Step,
    name: :cross_moria,
    description: "Crosses Moria with the packed provisions",
    input_schema: [
      provisions: [type: :map, required: true]
    ],
    output_schema: [
      moria: [type: :map, required: true]
    ]

  @impl Squidie.Step
  def run(%{provisions: provisions}, %Squidie.Step.Context{run_id: run_id}) do
    {:ok,
     %{
       moria: %{
         run_id: run_id,
         ring_id: provisions.ring_id,
         status: "crossed",
         lembas_left: provisions.lembas_count - 3
       }
     }}
  end
end
```

## Define A Workflow

A workflow declares the trigger, payload, steps, and transitions. The workflow
definition says what should happen; the journal records what did happen.

```elixir
defmodule SquidieLivebook.RingErrandWorkflow do
  use Squidie.Workflow

  workflow do
    trigger :leave_shire do
      manual()

      payload do
        field :ring_id, :string
      end
    end

    step :pack_lembas, SquidieLivebook.PackLembas
    step :cross_moria, SquidieLivebook.CrossMoria

    transition :pack_lembas, on: :ok, to: :cross_moria
    transition :cross_moria, on: :ok, to: :complete
  end
end
```

## Inspect The Workflow Spec

The DSL compiles into a normalized workflow spec. This is the data shape that
tooling can inspect without parsing the module source. It is also the contract
visual editors and runtime-authored workflows will build on.

```elixir
{:ok, spec} = Squidie.Workflow.to_spec(SquidieLivebook.RingErrandWorkflow)

%{
  workflow: spec.workflow,
  triggers: Enum.map(spec.triggers, &Map.take(&1, [:name, :type, :payload])),
  payload: spec.payload,
  steps: Enum.map(spec.steps, &Map.take(&1, [:name, :module, :opts])),
  transitions: spec.transitions,
  entry_steps: spec.entry_steps
}
```

`triggers` describe how a run can start. `payload` is the external input
contract. `steps` are executable workflow nodes. `transitions` describe durable
progression from one step outcome to the next node or to `:complete`.

For deeper authoring rules, see [Workflow Authoring](workflow_authoring.md).

## Start A Run

Manual triggers start through the public API. This workflow has one trigger, so
`start/3` uses it as the default trigger.

```elixir
{:ok, run} =
  Squidie.start(
    SquidieLivebook.RingErrandWorkflow,
    %{ring_id: "one-ring"},
    opts
  )

%{
  run_id: run.run_id,
  status: run.status,
  reason: run.reason,
  planned_runnables: run.planned_runnables,
  visible_attempts: Enum.map(run.visible_attempts, &SquidieLivebook.Output.attempt/1),
  scheduled_attempts: run.scheduled_attempts,
  next_visible_at: run.next_visible_at
}
```

The first snapshot already has one visible attempt: `pack_lembas`. Visible
attempts are work the host can claim now. Scheduled attempts are work that
exists but should not be claimed until `next_visible_at`.

## Execute Visible Work

Workers provide capacity by calling `Squidie.execute_next/1`. Each call claims
one visible journal attempt, runs the step, records the result, and makes the
next attempt visible when the workflow should continue.

A run is the whole workflow instance. An attempt is one executable unit of work
inside that run, such as the visible `:pack_lembas` or `:cross_moria` step.

```elixir
worker_opts = Keyword.put(opts, :owner_id, "livebook-worker")

{:ok, first_step} = Squidie.execute_next(worker_opts)
{:ok, completed_run} = Squidie.execute_next(worker_opts)
{:ok, no_more_work} = Squidie.execute_next(worker_opts)

%{
  first_step_status: first_step.status,
  first_step_reason: first_step.reason,
  visible_after_first_step: Enum.map(first_step.visible_attempts, &SquidieLivebook.Output.attempt/1),
  applied_after_first_step: first_step.applied_runnable_keys,
  completed_status: completed_run.status,
  completed_reason: completed_run.reason,
  completed_attempts: Enum.map(completed_run.attempts, &SquidieLivebook.Output.attempt/1),
  no_more_work: no_more_work
}
```

After the first call, the `pack_lembas` attempt has been applied and the
`cross_moria` attempt becomes visible. After the second call, the run is
terminal. The third call returns `:none` because this queue has no visible work
left.

## Start A Child Workflow

Steps can start child workflow runs when a larger workflow needs durable
fan-out. Child starts require an explicit `child_key`; calling the same child
start again from a retried parent step returns the same child run instead of
creating a duplicate.

This example keeps the parent and child on different queues so you can see that
the parent retry completes before the child is drained.

```elixir
defmodule SquidieLivebook.DeliverInvite do
  use Squidie.Step,
    name: :deliver_invite,
    description: "Delivers an invite from a child workflow",
    input_schema: [
      party_id: [type: :string, required: true],
      guest_id: [type: :string, required: true],
      fail_child_once: [type: :boolean, required: false]
    ],
    output_schema: [
      invite_delivery: [type: :map, required: true]
    ]

  @impl Squidie.Step
  def run(%{party_id: party_id, guest_id: guest_id} = input, %Squidie.Step.Context{
        attempt: attempt
      }) do
    if Map.get(input, :fail_child_once, false) and attempt == 1 do
      {:retry, %{message: "retry child delivery"}}
    else
      {:ok, %{invite_delivery: %{party_id: party_id, guest_id: guest_id, status: "delivered"}}}
    end
  end
end

defmodule SquidieLivebook.InviteDeliveryWorkflow do
  use Squidie.Workflow

  workflow do
    trigger :deliver_invite do
      manual()

      payload do
        field :party_id, :string
        field :guest_id, :string
        field :fail_child_once, :boolean, default: false
      end
    end

    step :deliver_invite, SquidieLivebook.DeliverInvite, retry: [max_attempts: 2]

    transition :deliver_invite, on: :ok, to: :complete
  end
end

defmodule SquidieLivebook.StartInviteChild do
  use Squidie.Step,
    name: :start_invite_child,
    description: "Starts a child invite workflow",
    input_schema: [
      party_id: [type: :string, required: true],
      guest_id: [type: :string, required: true],
      child_queue: [type: :string, required: true],
      fail_after_child_start: [type: :boolean, required: false],
      fail_child_once: [type: :boolean, required: false]
    ],
    output_schema: [
      invite_child: [type: :map, required: true]
    ]

  @impl Squidie.Step
  def run(
        %{party_id: party_id, guest_id: guest_id, child_queue: child_queue} = input,
        %Squidie.Step.Context{attempt: attempt} = context
      ) do
    child_key = "invite_#{guest_id}"

    with {:ok, child_run} <-
           Squidie.start_child_run(
             context,
             SquidieLivebook.InviteDeliveryWorkflow,
             %{
               party_id: party_id,
               guest_id: guest_id,
               fail_child_once: Map.get(input, :fail_child_once, false)
             },
             child_key: child_key,
             metadata: %{guest_id: guest_id},
             queue: child_queue
           ) do
      if Map.get(input, :fail_after_child_start, false) and attempt == 1 do
        {:retry, %{message: "retry after child start"}}
      else
        {:ok,
         %{
           invite_child: %{
             run_id: child_run.run_id,
             child_key: child_key,
             queue: child_queue,
             reused_after_retry?: attempt > 1
           }
         }}
      end
    else
      {:error, reason} ->
        {:error, reason}
    end
  end
end
```

```elixir
defmodule SquidieLivebook.NestedInviteWorkflow do
  use Squidie.Workflow

  workflow do
    trigger :nested_invite do
      manual()

      payload do
        field :party_id, :string
        field :guest_id, :string
        field :child_queue, :string
        field :fail_after_child_start, :boolean, default: true
        field :fail_child_once, :boolean, default: true
      end
    end

    step :start_invite_child, SquidieLivebook.StartInviteChild, retry: [max_attempts: 2]

    transition :start_invite_child, on: :ok, to: :complete
  end
end
```

```elixir
child_queue = "livebook-child"

{:ok, nested_parent} =
  Squidie.start(
    SquidieLivebook.NestedInviteWorkflow,
    %{
      party_id: "shire-party",
      guest_id: "frodo",
      child_queue: child_queue,
      fail_after_child_start: true,
      fail_child_once: true
    },
    opts
  )

{:ok, parent_retrying} = Squidie.execute_next(worker_opts)
{:ok, parent_completed} = Squidie.execute_next(worker_opts)

[%{child_run_id: child_run_id}] = parent_completed.child_runs
{:ok, nested_graph} = Squidie.inspect_run_graph(nested_parent.run_id, opts)
nested_graph_payload = Squidie.Runs.GraphInspection.to_map(nested_graph)

{:ok, child_retrying} =
  Squidie.execute_next(Keyword.merge(worker_opts, queue: child_queue))

{:ok, child_completed} =
  Squidie.execute_next(Keyword.merge(worker_opts, queue: child_queue))

%{
  parent_attempts: Enum.map(parent_completed.attempts, &SquidieLivebook.Output.attempt/1),
  parent_child_runs: parent_completed.child_runs,
  parent_child_links: nested_graph_payload.child_links,
  parent_context: parent_completed.context.invite_child,
  child_before_drain: child_run_id,
  child_retrying_attempts: Enum.map(child_retrying.attempts, &SquidieLivebook.Output.attempt/1),
  child_completed_attempts: Enum.map(child_completed.attempts, &SquidieLivebook.Output.attempt/1),
  child_parent_link: child_completed.parent_run
}
```

The parent attempted `start_invite_child` twice, but `child_runs` contains one
child because the second parent attempt reused the original `child_key`. The
child also retried once before completing, and its `parent_run` points back to
the parent step that created it. Graph inspection also exposes `child_links` so
UIs can render the parent-to-child subflow without treating the child workflow
as an inline node.

## Inspect The Run

Inspection reads durable journal facts. Use it for dashboards, support tools,
and operator-facing detail pages.

```elixir
{:ok, inspected} =
  Squidie.inspect_run(
    run.run_id,
    Keyword.merge(opts, include_history: true)
  )

%{
  status: inspected.status,
  reason: inspected.reason,
  context: inspected.context,
  planned_runnables: Enum.map(inspected.planned_runnables, &SquidieLivebook.Output.runnable/1),
  visible_attempts: Enum.map(inspected.visible_attempts, &SquidieLivebook.Output.attempt/1),
  scheduled_attempts: Enum.map(inspected.scheduled_attempts, &SquidieLivebook.Output.attempt/1),
  attempts: Enum.map(inspected.attempts, &SquidieLivebook.Output.attempt/1),
  next_visible_at: inspected.next_visible_at
}
```

`context` is the durable run context assembled from completed step outputs.
`attempts` is historical evidence. `visible_attempts` and `scheduled_attempts`
explain what can run now versus what needs a later wakeup.

## Inspect The Graph

Graph inspection gives UI builders a node and edge view of the same run.

```elixir
{:ok, graph} = Squidie.inspect_run_graph(run.run_id, opts)
graph_payload = Squidie.Runs.GraphInspection.to_map(graph)

%{
  source: graph.source,
  current_node_id: graph.current_node_id,
  nodes: Enum.map(graph_payload.nodes, &SquidieLivebook.Output.node/1),
  edges: Enum.map(graph_payload.edges, &SquidieLivebook.Output.edge/1),
  child_links: graph_payload.child_links
}
```

The graph contract is the shape a host UI can serialize after applying its own
authorization and redaction policy. See the
[Graph Inspection Contract](graph_inspection.md) for the full node, edge, and
child-link shape.

## Explain The State

Explanation condenses the run into a reason and diagnostics that are easier to
show to operators.

```elixir
{:ok, explanation} = Squidie.explain_run(run.run_id, opts)

%{
  status: explanation.status,
  reason: explanation.reason,
  summary: explanation.summary,
  next_actions: explanation.next_actions,
  evidence: explanation.evidence
}
```

Use explanation output when a support or operator surface needs to answer "what
is the runtime waiting for?" without exposing raw journal entries.

## See A Scheduled Wakeup

Wait steps turn workflow-scale delays into future-visible attempts. They are
useful when the workflow should continue later, while the journal remains the
source of truth.

```elixir
defmodule SquidieLivebook.RecordGandalfArrival do
  use Squidie.Step,
    name: :record_gandalf_arrival,
    description: "Records that the delayed rendezvous became visible",
    input_schema: [
      ring_id: [type: :string, required: true]
    ],
    output_schema: [
      rendezvous: [type: :map, required: true]
    ]

  @impl Squidie.Step
  def run(%{ring_id: ring_id}, %Squidie.Step.Context{}) do
    {:ok, %{rendezvous: %{ring_id: ring_id, status: "wizard arrived"}}}
  end
end

defmodule SquidieLivebook.GandalfRendezvousWorkflow do
  use Squidie.Workflow

  workflow do
    trigger :wait_for_wizard do
      manual()

      payload do
        field :ring_id, :string
      end
    end

    step :wait_for_gandalf, :wait, duration: 1_000
    step :record_gandalf_arrival, SquidieLivebook.RecordGandalfArrival

    transition :wait_for_gandalf, on: :ok, to: :record_gandalf_arrival
    transition :record_gandalf_arrival, on: :ok, to: :complete
  end
end

wakeup_time = DateTime.utc_now()
wakeup_opts = Keyword.merge(opts, queue: "livebook-wakeup", now: wakeup_time)
wakeup_worker_opts = Keyword.put(wakeup_opts, :owner_id, "livebook-worker")

{:ok, wakeup_run} =
  Squidie.start(
    SquidieLivebook.GandalfRendezvousWorkflow,
    %{ring_id: "one-ring"},
    wakeup_opts
  )

{:ok, scheduled_run} = Squidie.execute_next(wakeup_worker_opts)

%{
  run_id: wakeup_run.run_id,
  reason: scheduled_run.reason,
  visible_attempts: scheduled_run.visible_attempts,
  scheduled_attempts: Enum.map(scheduled_run.scheduled_attempts, &SquidieLivebook.Output.attempt/1),
  next_visible_at: scheduled_run.next_visible_at
}
```

The wait step completed, then Squidie scheduled `record_gandalf_arrival` for later.
Until `next_visible_at`, workers should see no visible work for that queue.

```elixir
{:ok, :none} = Squidie.execute_next(wakeup_worker_opts)

{:ok, completed_rendezvous} =
  Squidie.execute_next(Keyword.put(wakeup_worker_opts, :now, scheduled_run.next_visible_at))

%{
  status: completed_rendezvous.status,
  reason: completed_rendezvous.reason,
  attempts: Enum.map(completed_rendezvous.attempts, &SquidieLivebook.Output.attempt/1)
}
```

## Add A Human Approval Boundary

Approval steps pause the workflow until an operator approves or rejects the run.
This is durable workflow state, not a transient process wait.

```elixir
defmodule SquidieLivebook.RecordCouncilApproval do
  use Squidie.Step,
    name: :record_council_approval,
    description: "Records an approved errand",
    input_schema: [
      ring_id: [type: :string, required: true],
      approval: [type: :map, required: true]
    ],
    output_schema: [
      recorded: [type: :map, required: true]
    ]

  @impl Squidie.Step
  def run(%{ring_id: ring_id, approval: approval}, %Squidie.Step.Context{}) do
    {:ok, %{recorded: %{ring_id: ring_id, decision: approval.decision}}}
  end
end

defmodule SquidieLivebook.RecordCouncilRejection do
  use Squidie.Step,
    name: :record_council_rejection,
    description: "Records a rejected errand",
    input_schema: [
      ring_id: [type: :string, required: true],
      approval: [type: :map, required: true]
    ],
    output_schema: [
      recorded: [type: :map, required: true]
    ]

  @impl Squidie.Step
  def run(%{ring_id: ring_id, approval: approval}, %Squidie.Step.Context{}) do
    {:ok, %{recorded: %{ring_id: ring_id, decision: approval.decision}}}
  end
end

defmodule SquidieLivebook.CouncilApprovalWorkflow do
  use Squidie.Workflow

  workflow do
    trigger :council_review do
      manual()

      payload do
        field :ring_id, :string
      end
    end

    approval_step :wait_for_council, output: :approval
    step :record_council_approval, SquidieLivebook.RecordCouncilApproval
    step :record_council_rejection, SquidieLivebook.RecordCouncilRejection

    transition :wait_for_council, on: :ok, to: :record_council_approval
    transition :wait_for_council, on: :error, to: :record_council_rejection
    transition :record_council_approval, on: :ok, to: :complete
    transition :record_council_rejection, on: :ok, to: :complete
  end
end

approval_opts = Keyword.put(opts, :queue, "livebook-approval")
approval_worker_opts = Keyword.put(approval_opts, :owner_id, "livebook-worker")

{:ok, approval_run} =
  Squidie.start(
    SquidieLivebook.CouncilApprovalWorkflow,
    %{ring_id: "one-ring"},
    approval_opts
  )

{:ok, paused_run} = Squidie.execute_next(approval_worker_opts)

{:ok, paused_explanation} = Squidie.explain_run(approval_run.run_id, approval_opts)

{:ok, resumed_run} =
  Squidie.approve(
    approval_run.run_id,
    %{actor: "ops_123", comment: "looks good"},
    approval_opts
  )

{:ok, approved_run} = Squidie.execute_next(approval_worker_opts)

%{
  paused_status: paused_run.status,
  paused_reason: paused_run.reason,
  manual_state: paused_run.manual_state,
  paused_summary: paused_explanation.summary,
  paused_next_actions: paused_explanation.next_actions,
  resumed_status: resumed_run.status,
  visible_after_approval: Enum.map(resumed_run.visible_attempts, &SquidieLivebook.Output.attempt/1),
  completed_status: approved_run.status,
  manual_state_after_resume: resumed_run.manual_state
}
```

The paused snapshot exposes `manual_state`, so a host UI can render the
operator decision boundary. `approve/3` records the decision and makes the
approval path visible. The next worker execution runs `record_council_approval`
and finishes the workflow.

## What To Read Next

- [Getting Started](https://github.com/dark-trench/squidie/blob/main/docs/getting_started.md)
- [Workflow Authoring](https://github.com/dark-trench/squidie/blob/main/docs/workflow_authoring.md)
- [Reference Workflows](https://github.com/dark-trench/squidie/blob/main/docs/reference_workflows.md)
- [Graph Inspection Contract](https://github.com/dark-trench/squidie/blob/main/docs/graph_inspection.md)
- [Host App Integration](https://github.com/dark-trench/squidie/blob/main/docs/host_app_integration.md)
- [Runtime Architecture](https://github.com/dark-trench/squidie/blob/main/docs/jido_runtime_architecture.md)
