## Documentation Index

Fetch the complete documentation index at: [/llms.txt](https://docs.firstresonance.io/llms.txt)

Use this file to discover all available pages before exploring further.

Read [Create an action](https://docs.firstresonance.io/automate-with-ion/ion-actions/manage-actions#create-an-action) before working through these examples. Each one below gives the field values for one ION Action: its title, target, and event, followed by the context query it reads and the code it runs.

## Block a status transition based on a related collection

Stops an issue from moving to In Review while any of its redlines sit on a step that isn’t complete or canceled. The technique: read a related collection from the context and require every record in it to be in an acceptable state.

| Field | Value |
| --- | --- |
| Title | Open redlines prevent moving issue into In Review |
| Target | `ISSUE` |
| Event | `UPDATE` |

**Context**

```
{
  issue(id: $id) {
    title
    status
    redlines { id step { status } }
  }
}
```

**Code**

```
if (
    context.get('issue', {}).get('status') == 'IN_REVIEW'
    and context.get('issue', {}).get('redlines') is not None
    and not all(
        redline.get('step', {}).get('status') in ['COMPLETE', 'CANCELED']
        for redline in context.get('issue', {}).get('redlines')
    )
):
    raise ValidationError()
```

## Restrict an action to specific roles

Blocks moving an issue back to In Review from a resolved state unless the acting user holds a quality role. The technique: branch on the acting user’s roles from `currentUser`, and read the prior value from `changes` to detect the reopen.

| Field | Value |
| --- | --- |
| Title | No permission to reopen a closed issue |
| Target | `ISSUE` |
| Event | `UPDATE` |

**Context**

```
{
  issue(id: $id) {
    title
    status
  }
}
```

**Code**

```
allowed_roles = {'Quality Engineer', 'Quality Inspector', 'Reliability Engineer', 'admin'}
if (
    context.get('issue', {}).get('status') == 'IN_REVIEW'
    and context.get('changes', {}).get('issues', {}).get('status', {}).get('old') == 'resolved'
    and not set(context.get('currentUser', {}).get('roles', [])).intersection(allowed_roles)
):
    raise ValidationError()
```

## Check a custom attribute

Stops a part from being added to a kit when its `On CAPA?` attribute is set. The technique: scan a list of `{ key, value }` attributes on a related object for a specific key.

| Field | Value |
| --- | --- |
| Title | Cannot add CAPA part to kit |
| Target | `PARTKITITEM` |
| Event | `CREATE` |

**Context**

```
{
  partKitItem(id: $id) {
    part {
      attributes { key value }
    }
  }
}
```

**Code**

```
if any(
    attr.get('key') == 'On CAPA?' and attr.get('value') is not None
    for attr in context.get('partKitItem', {}).get('part', {}).get('attributes', [])
):
    raise ValidationError()
```

## Require a field on every item in a collection

Blocks moving a procedure to In Review when any step has no upstream or downstream dependency. The technique: gate on a status change read from `changes`, then iterate the object’s child collection and require a field on each.

| Field | Value |
| --- | --- |
| Title | Check if step has dependencies |
| Target | `PROCEDURE` |
| Event | `UPDATE` |

**Context**

```
{
  procedure(id: $id) {
    id
    steps {
      upstreamStepIds
      downstreamStepIds
    }
  }
}
```

**Code**

```
status_change = context.get('changes', {}).get('procedures', {}).get('status', {})
if status_change.get('new') == 'in_review' and any(
    not (step.get('upstreamStepIds') or step.get('downstreamStepIds'))
    for step in context.get('procedure', {}).get('steps', [])
):
    raise ValidationError()
```

## Match records across a nested collection

For a Build procedure, blocks the move to In Review until reviewers from the Responsible Engineer, Production, and Mission Assurance teams have been added. The technique: flatten a nested collection (reviewers, then their teams) and confirm every required value is present.

| Field | Value |
| --- | --- |
| Title | Procedure requires the correct teams to review |
| Target | `PROCEDURE` |
| Event | `UPDATE` |

**Context**

```
{
  procedure(id: $id) {
    attributes { key value }
    reviewRequests { id status reviewer { id teams { name } roles { name } } }
  }
}
```

**Code**

```
status_change = context.get('changes', {}).get('procedures', {}).get('status', {})
procedure = context.get('procedure', {})
is_build = any(
    attr.get('key') == 'Procedure Type' and attr.get('value') == 'Build'
    for attr in procedure.get('attributes', [])
)
required_teams = ['Responsible Engineer', 'Production', 'Mission Assurance']
reviewer_team_names = [\
    team.get('name', '')\\
    for review in procedure.get('reviewRequests', [])\
    for team in review.get('reviewer', {}).get('teams', [])\
]
has_required = all(
    any(required in name for name in reviewer_team_names)
    for required in required_teams
)
if status_change.get('new') == 'in_review' and is_build and not has_required:
    raise ValidationError()
```

## Require a field before a status change

Blocks a purchase order line from moving to Requested when it has no need date. The technique: gate on a status change read from `changes`, then require a scalar field on the object.

| Field | Value |
| --- | --- |
| Title | Purchase line items must have a need date |
| Target | `PURCHASEORDERLINE` |
| Event | `UPDATE` |

**Context**

```
{
  purchaseOrderLine(id: $id) {
    id
    needDate
  }
}
```

**Code**

```
status_change = context.get('changes', {}).get('purchaseOrderLines', {}).get('status', {})
if (
    status_change.get('new') == 'requested'
    and context.get('purchaseOrderLine', {}).get('needDate') is None
):
    raise ValidationError()
```

To enforce purchase order approval levels, use the built-in [purchase order approvals](https://docs.firstresonance.io/manage-supply-chain/purchasing/approve-purchase-orders) instead of an ION Action. It’s easier to configure and maintain.

## Require multiple fields on creation

Blocks creating a part of type Part until the required fields are filled in. The technique: run on the `CREATE` event, guard on the object’s type, then check a list of required fields at once.

| Field | Value |
| --- | --- |
| Title | Required fields for part creation |
| Target | `PART` |
| Event | `CREATE` |

**Context**

```
{
  part(id: $id) {
    id
    revision
    description
    trackingType
    sourcingStrategy
    partType
    unitOfMeasure { id }
  }
}
```

**Code**

```
part = context.get('part', {})
required_fields = ['revision', 'description', 'trackingType', 'sourcingStrategy', 'unitOfMeasure']
if part.get('partType') == 'PART' and any(not part.get(field) for field in required_fields):
    raise ValidationError()
```
