# Structs

> Reuse · https://spoletta.com/docs/structs

A struct is a single object defined once, for many flows to use: its states, attributes and parts. When several flows exchange the same thing, such as a *Ticket* with a key, a summary and a priority, a struct keeps that shape in one place.

*Diagram: One Ticket struct, used by two flows through copies. Change the struct and both copies change.*

## Why structs

An object's shape is a contract: an agent fills in exactly its attributes, a script prints exactly those keys, a person's form asks for exactly those fields. When two flows hand the same thing to each other, or both work on the same kind of record, the contract should be the same in both. Drawing it twice lets the two drift apart; a struct can't.

## Making a struct

1.  **Create it**
    
    On the flow list, type a name under **Structs** and press **New struct**.
    
2.  **Shape the object**
    
    A struct's board holds only its one object, unfolded: there is no Start, no End, no processes and no runs. Add attributes, with their definitions, in its settings; add parts on its board as for any [unfolded object](https://spoletta.com/docs/structural-links.md); add states with right-click, **Add state**, and their rules under **State rules**.
    
3.  **Name it**
    
    The struct's name is its object's name. Renaming the struct renames the object.
    

## Using a struct

Structs are listed under **Structs** in the board's sidebar. Drag one onto a board and you get a **copy** of its object, which you link like any other object. Its subtitle says where it comes from, and its settings link to the struct.

A copy follows its source:

| Set in the struct | The copy's own |
| --- | --- |
| Name, quantity, states, state rules, attributes, parts, what it specializes | Where it sits on the board, its width, and whether it is stored (Store what runs generate), unless the struct [shares its records](https://spoletta.com/docs/structs.md#shared-records) |

In the copy, **Rename** and **Add state** are disabled and the settings fields are locked. Links name its states as the struct names them. On the flow list, a struct's card says how many flows use it, or *Not used yet*.

> **Deleting a struct:** A struct can be deleted while flows use it. Its copies there then become objects of their own: they keep their shape, but no longer follow anything.

## Shared records

Normally each flow keeps its own copy's values to itself. Tick **Share its records with every flow using it** on a struct's object and every copy of it, in whatever flow, reads and writes **one store of records**, like a table the flows have in common. Use it when one flow adds things and another works on them, such as one fetching Sentry issues and another fixing them. A shared object, and every copy of it, wears a round database badge on its top right corner, as a stored object does; hovering over it says *Shared*.

-   The object needs a **Unique** attribute first: it is what tells records apart. A record written again with the same value is the same record.
-   A process that only **generates** the object, such as a Sentry step or a fetching script, adds the records that are new, in the state its link names, and refreshes the fields of those it knows, **keeping their state**. Fetching an issue again doesn't undo how far it got. Name the state new records start in on the link, rather than by a rule that holds of every record.
-   A process that also **takes** it, by any input link or a [query](https://spoletta.com/docs/procedural-links.md#looking-up-stored-records), moves the records it generates into the state they come out in: the one named on the link, one a rule gives, or one an agent decides. Fields it leaves out stay as they were.
-   A list writes a record per item, all in the state the list comes out in.
-   Which records get worked on, or worked on again, is up to **queries**. A query reads the records in a state, whichever flow wrote them, and a rule on their fields narrows them, such as `attempts < 3`.

> **No run claims a record:** Records stay once the step writing them is done, whatever its run does later. No run claims a record, so two runs querying the same state at once may take the same one. Don't let the working flow's runs overlap: a schedule skips a time while its last run goes on.

### An example: fetch in one flow, fix in another

*Diagram: Two flows, one store of Exception records: Fetch adds them as new, Fix takes the oldest new one and leaves it completed, and Report leaves one Fix couldn't fix as failed.*

A struct *Exception* is a shared list with a unique `id` and the states `new`, `completed` and `failed`.

-   **Fetch exceptions**, scheduled every hour, has a Sentry step yielding Exception in `new`. Issues it has seen before keep whatever state they reached.
-   **Fix exceptions**, scheduled every ten minutes, has an agent *Fix* that queries the oldest record in `new` and yields Exception in `completed`.
-   Fix's failure handler, *Report*, takes Exception and yields it in `failed`. To try failed ones again, query `failed` too, narrowed by a rule such as `attempts < 3`.

spoletta-setup.yaml:

```yaml
structs:
  - name: Exception
    shared: true
    many: true
    states: [new, completed, failed]
    attributes:
      - name: id
        unique: true
      - name: title
      - name: permalink
flows:
  - name: Fetch exceptions
    start: {next: Fetch}
    objects:
      - copyOf: {flow: Exception}
    processes:
      - name: Fetch
        kind: sentry
        sentry: {organization: acme, project: shop}
        yields:
          - object: Exception
            state: new
  - name: Fix exceptions
    start: {next: Fix}
    objects:
      - copyOf: {flow: Exception}
    processes:
      - name: Fix
        prompt: Find the cause of the exception in Exception and fix it.
        takes:
          - object: Exception
            query: {state: new, limit: 1, oldest: true}
        yields:
          - object: Exception
            state: completed
        onFailure: [Report]
      - name: Report
        prompt: Say on the Sentry issue why Exception couldn't be fixed.
        takes: [Exception]
        yields:
          - object: Exception
            state: failed
```

A shared object's copy can't also be stored in its own flow's runs (**Store what runs generate**), and in a setup file `shared: true` goes on the struct, never together with `persistent`.

OPL:

```text
Exception is shared, so every flow using it reads and writes the same records.
Fix looks up the earliest 1 stored records of Exception in new.
Fix yields Exception as completed.
```

### The records table

The struct's board lists its records in the sidebar under **Records**, and the **Records** button on its toolbar opens them as a table, at `#/flows/<struct>/records`.

-   One row per record, newest first: its number, its state, a column for each field in the order the board has them, when it was last updated, and a link to the run that wrote it, on that run's own flow.
-   The query bar searches every stored record, up to 20 000, such as `state:new title:*timeout*`. Facets on the left count each column's values, the *State* facet included, with *no value* for records without a state. Click a column's name to sort by it. It all works as on a flow's Data page: see [Searching and sorting the data](https://spoletta.com/docs/running.md#searching-and-sorting-the-data).
-   Click a cell to see all of its value.
-   The table shows the latest 1000 matching records and refreshes every ten seconds.

## Struct or procedure?

|     | Struct | Procedure |
| --- | --- | --- |
| Shares | The shape of one object | A piece of flow: steps and the objects between them |
| Runs | Never | As a run of its own, whenever a caller reaches it |
| Used through | A copy of its object | A procedure process, plus copies of its outcomes |
| Good for | Records several flows exchange: a Ticket, a Change, a Release | Work several flows repeat: announce, triage, open a PR |

## In a setup file

```yaml
structs:
  - name: Ticket
    attributes:
      - name: key
        definition: The Jira key, such as SHOP-123.
      - name: summary
      - name: priority
    states: [bug, feature]
flows:
  - name: Triage ticket
    objects:
      - copyOf: {flow: Ticket}
```

A copy lists none of its source's fields, and its name may be left out. See [Setup files](https://spoletta.com/docs/setup-files.md).
