---
description: The nine material states, which stage produces each one, which are terminal, and how a material moves or returns between them.
---

# Material states

Every candidate material carries exactly one state, and that state is what the
orchestrator claims work from. There are nine, and knowing which three are
terminal explains most of what `status` shows you.

## The nine

| State | Set by | Terminal | Meaning |
|---|---|---|---|
| `NEW` | seeding, or fan-out adoption | no | An element-combination seed, or a freshly adopted child, waiting for its first stage. |
| `COMPOSED` | `compose` | no | A charge-balanced composition exists. |
| `STRUCTURED` | `structure` | no | A candidate crystal structure exists. |
| `SCREENED` | `screen` | no | The MACE relaxation converged and the verdict was `RELAXED`. |
| `CALCULATED` | `calculate` | no | The DFTB+ WorkChain converged. Runs without the optional stage never park here. |
| `EXTRACTED` | `extract` | no | `record.json` is written. |
| `DONE` | finalize | **yes** | The material completed the configured funnel. |
| `FAILED` | a verdict or an exhausted budget | **yes** | Terminally rejected or terminally faulted; `status` shows the stage and the reason. |
| `EXPANDED` | fan-out settlement | **yes** | A parent whose children were all adopted. Not a failure. |

The six non-terminal states form one chain, in funnel order:

    NEW → COMPOSED → STRUCTURED → SCREENED → CALCULATED → EXTRACTED → DONE

A stage claims materials in the state before it and completes them into the
state after it. Movement along the chain is **strictly forward** — nothing
regresses, and a completion that has already been recorded is a no-op, so an
interrupted run replays safely.

**A skipped stage skips its state.** With no `calculate:` in your config,
`screen` completes a material straight from `SCREENED` to `EXTRACTED`; nothing
ever parks in `CALCULATED`. The orchestrator derives each stage's input state
from the funnel you configured, not from a fixed position in the chain.

## The three terminal states, and why they differ

**`DONE` is retirement, and it is deliberate.** A material becomes `DONE` only
when the run executes through the **configured last stage**. A run stopped
mid-funnel — by `pipeline.stop_after` or by `run --only <stage>` — leaves its
materials **resting** at that stage's output state, live and claimable, so a
later run continues them in the same workspace. This is why a `stop_after:
structure` run leaves everything at `STRUCTURED` rather than retiring it: those
materials still have a funnel ahead of them.

**`FAILED` covers two different things**, and the difference is visible in the
attempt count:

| Cause | Attempts | Example |
|---|---|---|
| A deterministic science verdict | `1` | MACE relaxed the cell and the result is unphysical; a DFTB+ WorkChain exhausted the plugin's own healing ladders. |
| A fault that spent `retry_budget` | `retry_budget` | A missing input file, a crashed job, an unreachable backend. |

A verdict spends no budget, because re-running reproduces it. A fault might not
be reproducible, so it is retried — and when in doubt, the tool treats an
outcome as a fault rather than a verdict. Both end in `FAILED`, and `matmelt
retry` brings either back.

**`EXPANDED` is a success.** `compose` and `structure` are fan-out stages: one
seed becomes several compositions, one composition becomes several candidate
crystals. The parent has no further work of its own, so it retires as `EXPANDED`
once **all** its children are adopted. Its children ride the funnel individually,
each addressable by id, and `run --materials <parent-id>` reaches them through
lineage.

## Seeing where materials are

```console
$ matmelt status
$ matmelt status --state STRUCTURED
$ matmelt status --json
```

`status` alone gives the tally per state, the failures with their stage and
reason, and any jobs in flight. `--state <STATE>` lists the material **ids** in
one state — the picking list for `run --materials`. `monitor` shows the same
information as a live dashboard.

## Moving materials back

Terminal is terminal: no stage claims a `DONE`, `FAILED` or `EXPANDED`
material. One command deliberately reverses that, and only for failures:

```console
$ matmelt retry --dry-run
$ matmelt retry --stage structure --yes
```

`retry` resets `FAILED` materials to the state they should resume from, derived
per material from **its own completion timeline** — the output state of the last
stage it actually completed, or `NEW` if it never completed one. That derivation
uses no config, so it is right even if you have since changed the funnel. The
reset clears the lease, the attempt count and the recorded error, and
**preserves the completions timeline**, so the material's history stays intact.

Nothing resets `DONE` or `EXPANDED`. A finished material is re-run by pointing a
stage at it explicitly (`run --only <stage> --materials <id>`), which is a
different operation with a different meaning.

## Durability

State lives in a single SQLite database under `run.work_dir`, with the
write-ahead log on and every read-then-write in an immediate transaction.
Claiming is atomic, so a material is never worked twice; a claim that outlives
`pipeline.lease_timeout_s` returns to its queue.

Alongside state, an **in-flight ledger** records every submitted job's durable
handle — a PID or a scheduler job id — at the moment of submission. That is what
makes an interrupted run resumable: a fresh process re-polls the jobs the old one
started instead of launching duplicates, so work already running on a cluster is
picked back up rather than repeated.
