# Modules

A module is a parameterised sequence of steps a plan calls as one item. Fixing the module fixes every plan that uses it.

Source: https://opafra.com/docs/plans/modules

---

A module is a reusable, parameterised sequence of items. A plan references it as a single
item, and it expands into its steps when the run executes.

When the same four steps appear in six plans, that is a module. Fixing it once fixes all
six.

## Calling one

A plan calls a module the way it calls a tool, with `module:` and the module's slug:

```yaml title="a plan step calling a module"
- name: Deploy the app
  targets: [production]
  tool: module:app-deploy
  args:
    repo: git@github.com:acme/api.git
    release_dir: /srv/api
    service: api
```

Those arguments are the module's declared **parameters**. Each one has a type, and can be
required or carry a default, so calling a module wrongly is caught rather than producing
a step with an empty path in it.

## What a module declares

Three things:

**Items** are the ordered work it expands into, each one an ordinary item with a tool,
arguments, and optionally its own condition, timeout and capture.

**Parameters** are the interface. The calling plan fills them in, and they are available
inside the module as `{{ params.x }}`.

**Outputs** are what the module promises to produce, so a later step in the calling plan
can consume them.

> **Note**
>
> A declared output must actually be captured by one of the module's items. This is checked
> when the module is written and again when it expands, so a module that promises an output
> it never produces fails loudly rather than handing the next step an undefined value.

Two instances of the same module in one plan get their outputs namespaced separately, so
calling `app-deploy` twice for two services does not have the second overwrite the first.

## Expansion happens at run time

A module is not copied into the plan when you write it. It expands when the run executes,
which means **editing a module changes every plan that uses it, on their next run**.

That is the point, and it is also the thing to be careful about. A fix to a module
propagates everywhere without touching a single plan. So does a mistake.

> **Warning**
>
> There is no version pinning. A plan does not record which revision of a module it ran
> last time, and cannot be held to one. Dry run the plans that matter after changing a
> shared module, rather than assuming a plan that worked yesterday is unchanged.

## The shipped library

Opafra ships a curated library of modules under the `opafra.` prefix, visible to everyone
and read-only. They cover the sequences most teams write on their first week.

Your own module with the same slug **shadows** the shipped one. So the way to adapt a
library module is to create your own with that slug, and every plan calling it picks
yours up without being edited.

Shipped modules carry a version, so you can see which revision you are running.

## When to extract one

Extract when the same sequence appears in a third plan, not the second. Two copies are
cheap to keep in step; three is where drift starts and where the copies begin to disagree
about details nobody wrote down.

Extract also when a sequence encodes a decision you want made once: the order of a
graceful restart, which paths get backed up, how long to wait before declaring a rollout
failed. A module is where that decision lives so it stops being re-argued per plan.

Do not extract a single tool call. A module around one item adds a layer without adding a
decision, and the plan reads better calling the tool directly.

## Next steps

- [Plan structure](/docs/plans/structure) for parameters, capture and conditions in full
- [Tool catalogue](/docs/reference/tools) for what a module's items can call
