12. Triggers

A trigger is an automated rule that watches for a specific event in a project and, when the event occurs and an optional filter condition is met, executes a configured action automatically. Triggers are managed in the project menu via the Triggers button.

Common use cases include automatically organising newly imported data into folders, launching a processing task whenever a new series arrives, or copying studies to an archive project.

../../../_images/trigger_overview.png

12.1. Overview

Each trigger has three parts:

  • Event — what causes the trigger to fire (e.g. a new series arriving, a scheduled time).

  • Selector — an optional filter that limits which objects the trigger acts on.

  • Action — what Agora does when the trigger fires and the selector matches.

When multiple triggers watch the same event, they execute in the order shown in the trigger list. The order can be changed by dragging rows in the list. Triggers execute one at a time, in sequence — later triggers see the results of earlier ones (e.g. a rename applied by the first trigger is visible to the second).

Note

If a trigger with a Move to project action fires, all subsequent triggers in the list are skipped for that event. The object has left the project at that point, so any further actions would operate on an object that no longer belongs here.


12.2. Events

The following events are available:

Event

When it fires

Add Study to Project

Once when a study record is first created in the project (on import, copy, or move).

Add Series to Project

Once per series each time a series is added to the project (on import, copy, or move).

Add Dataset to Project

Once per dataset when a standalone dataset is added to the project, or for each dataset belonging to a series on import.

After Task Completed

Fires when a task finishes successfully. An optional task filter limits the event to a specific task; leaving it empty triggers on any task completion.

Remove Study from Project

Fires when a study is removed (soft-deleted) from the project.

Remove Series from Project

Fires when a series is removed (soft-deleted) from the project.

Tag Added

Fires when a tag is applied to any object in the project (study, series, or dataset). An optional tag filter limits the event to a specific tag; leaving it empty triggers on any tag being added.

Periodic

On a recurring schedule (hourly, daily, weekly, monthly, or once).

Manual

Triggered manually from the task menu.

Important

Understanding imports: studies arrive series by series

When data is imported into Agora the transfer happens at the series level — each series is received, processed, and stored independently, in parallel or in sequence, depending on the scanner and protocol. This has an important consequence for which event to use:

  • Add Series to Project fires once for each series the moment that series finishes importing. This is the correct event for reacting to incoming data — for example, launching a processing task or organising data into folders. The context includes context['series'] (the triggering series) and context['exam'] (the full parent study with all series imported so far), which gives access to study-level metadata (patient demographics, study description, accession number, etc.) during per-series processing. Since the trigger fires multiple times, use idempotent actions (adding a tag that is already there has no effect).

  • Add Study to Project fires once when the study record is first created in the project, which typically happens when the very first series of the study arrives. At that point the remaining series are still being imported. However, by combining this event with a delay, you can wait for the import to finish before acting — making it the right choice for checking study completeness, for example tagging a study as complete once all expected series are present. See `Add Study / Series / Dataset to Project — optional delay`_ below.

When a study is copied or moved to a project, Agora transfers the entire study atomically. In this case both events fire with the complete study available. The Add Study to Project event is therefore also suitable for reacting to copy/move operations or for study-level bookkeeping (e.g. study renaming).

Summary:

  • React to imports (per-series processing, folder organisation) → use Add Series to Project

  • Check study completeness after an import finishes → use Add Study to Project with a delay

  • React to copy/move of a complete study → Add Study to Project or Add Series to Project both work

  • React to individual datasets (e.g. NIfTI, Logfiles, pdf’s etc.) → use Add Dataset to Project

Add Study / Series / Dataset to Project — optional delay

When one of the three Add … to Project events is selected, an optional delay can be configured. The action will not execute immediately when the event fires; instead it is scheduled to run after the specified number of seconds, minutes, or hours. This is useful when you want to wait for additional data to arrive before acting — for example, waiting a few minutes after a series arrives to give the scanner time to send all related datasets.

Note

The delay is applied per trigger. If the same object matches several triggers with different delays, each trigger fires independently according to its own delay.

After Task Completed — optional task filter

When After Task Completed is selected, an optional task dropdown appears. Select a specific task to limit the trigger to completions of that task only. Leave the field empty to fire on any task completion.

Tag Added — optional tag filter

When Tag Added is selected, an optional tag dropdown appears. Select a specific tag to limit the trigger to additions of that tag only. Leave the field empty to fire whenever any tag is added to any object.

Periodic trigger schedule

When Periodic is selected, the following fields control the schedule:

  • Start time — date and time of the first execution. Must be set to a future time when the trigger is saved.

  • Interval typeHourly, Daily, Weekly, Monthly, or One-time.

  • Interval — how many units to wait between runs (e.g. 3 with Hourly fires every 3 hours).

A Run now button (▶) is shown next to the interval settings once the trigger has been saved. Clicking it executes the trigger immediately without affecting the regular schedule or updating the last-run timestamp used for the next scheduled execution.


12.3. Selectors

A selector decides whether the trigger acts on a given object. Three selector modes are available:

Selector

Behaviour

No Filter

The trigger acts on every object that matches the chosen event.

Regular Expression

Only objects whose name matches a given regular expression are processed. Not applicable to Periodic triggers (there is no object name to match).

Python filter

A Python script is executed. The trigger fires only if the script returns True. The context available to the script depends on the event type — see The context dictionary.


12.4. Python Script Filter

The script filter lets you write arbitrary Python logic to decide whether the trigger should act on a given object. This is useful when the condition cannot be expressed as a simple name pattern — for example, filtering by acquisition parameter, scanner name, or patient metadata.

12.4.1. Script structure

The script must define a function named filter that accepts a single context argument and returns a boolean:

def filter(context: dict) -> bool:
    # inspect context fields here
    return True   # True → process this object; False → skip it

The script is executed in a sandboxed environment (an isolated container with no network access) with a 25-second timeout. Only the Python standard library is available — no third-party packages.

12.4.2. The context dictionary

The context dictionary contains keys that depend on the event type. The primary key is named after the triggering object; additional top-level keys may also be present.

Event

Context keys and what they contain

Add Study to Project

context['exam'] — full study object including nested patient, project, and a series list. Each series contains a datasets list, and each dataset contains a parameters list.

Add Series to Project

context['series'] — the series that was added, with a datasets list and a nested exam parent. Each dataset contains a parameters list.

context['exam'] — the full parent study object including all series currently in the study (same structure as Add Study to Project). Use this to check whether all required series have arrived.

Add Dataset to Project

context['dataset'] — the dataset, with a parameters list and flat serie / exam parent references.

After Task Completed

Same context as the event that originally triggered the task. context['exam'], context['series'], and context['datasets'] are populated if the completed task had associated objects; otherwise None / empty list.

Remove Study from Project

context['exam'] — the study being removed. Same fields as Add Study to Project (patient, project, and series list are included).

Remove Series from Project

context['series'] — the series being removed. Same fields as Add Series to Project.

Tag Added

The context key depends on the type of object that was tagged: context['exam'] for studies, context['series'] for series, or context['dataset'] for datasets. Only the key matching the tagged object type is populated.

Periodic

No triggering object. The context contains project and schedule metadata instead:

context['project']{'id', 'name', 'description'}

context['timestamp']{'year', 'month', 'day', 'hour', 'minute', 'iso'}

context['last_run'] — ISO datetime string of the previous execution, or None on first run.

context['exams'] — full exam tree for the project (only loaded when the script text contains the word exams outside of comments; same lazy-load as parameters).

Manual

Same as the equivalent data event — context['exam'], context['series'], or context['dataset'] depending on which objects were selected when the trigger was run manually. Context may be empty if no objects were selected.

context['exam'] fields (Add Study to Project)

Field

Description

id

Internal study ID.

name

Study name.

uid

Unique identifier (e.g. DICOM Study Instance UID).

description

Study description.

scanner_name

Name of the scanner on which the study was acquired.

vendor

Scanner vendor / manufacturer name.

start_time

Acquisition start time (ISO 8601 string).

created_date

Date the study was added to Agora (ISO 8601 string).

is_anonymized

True if the study has been anonymized.

patient

Nested patient object (see below).

project

Nested project object (see below).

series

List of series objects. Each series has a datasets list, and each dataset has a parameters list (see below).

context['exam']['patient'] fields

Field

Description

id

Internal patient ID.

name

Patient name.

patient_id

Patient identifier (e.g. hospital number).

birth_date

Date of birth (ISO 8601 string).

sex

Sex: "m", "f", or "o" (other).

weight

Body weight in kg.

created_date

Date the patient record was created (ISO 8601 string).

context['exam']['project'] fields

Field

Description

id

Internal project ID.

name

Project name.

description

Project description.

context['series'] fields (Add Series to Project)

Field

Description

id

Internal series ID.

uid

Unique identifier (e.g. DICOM Series Instance UID).

name

Series name.

time

Series acquisition time (ISO 8601 string).

is_refscan

True if this series is a reference scan.

is_coil_survey

True if this series is a coil survey.

acquisition_number

DICOM acquisition number.

datasets

List of dataset objects belonging to this series. Each dataset has a parameters list (see below).

exam

Parent study object (flat reference — no series list). For the full exam including all current series, use the top-level context['exam'] key instead.

context['dataset'] fields (Add Dataset to Project)

Field

Description

id

Internal dataset ID.

name

Dataset name (typically the original filename).

type

Dataset type as an integer. See Dataset type values below.

mime_type

MIME type string.

parameters

List of acquisition parameters (see below).

serie

Flat parent series object (or None if the dataset has no parent series). Contains exam nested inside it.

exam

Flat direct parent study object (or None). Same fields as the flat exam described above.

Dataset type values

Value

Type

0

None / unknown

100

Philips Raw

101

Philips PAR/REC

102

Philips Spectroscopy

103

Philips ExamCard

104

Philips SIN file

200

Bruker Raw

201

Bruker Subject

202

Bruker Image

300

DICOM

400

Siemens Raw

401

Siemens Pro

500

ISMRMRD

600

NIfTI-1

601

NIfTI-2

602

NIfTI / Analyze 7.5

700

jMRUI Spectroscopy

800

AEX

10000

Query

100000

Other

The parameters list

For Add Series to Project, parameters are found on each dataset: context['series']['datasets'][n]['parameters']. For Add Dataset to Project, parameters are directly on the dataset: context['dataset']['parameters']. For Add Study to Project, parameters are nested deep: context['exam']['series'][n]['datasets'][m]['parameters'].

Each entry in the list is a dict with two keys:

Key

Description

Name

Parameter name string — e.g. "RepetitionTime", "ProtocolName", "(0018,0080)".

Value

Parameter value (type depends on the parameter — numeric or string).

Example of iterating parameters for a series trigger:

def filter(context):
    for dataset in context.get('series', {}).get('datasets', []):
        for param in dataset.get('parameters', []):
            if param['Name'] == 'RepetitionTime':
                return param['Value'] > 2000
    return False

12.4.3. Script filter examples

1. Filter by scanner name (Add Series to Project)

Only process series acquired on a specific scanner. The exam is accessed via series.exam:

def filter(context):
    exam = context.get('series', {}).get('exam') or {}
    return exam.get('scanner_name') == 'GYRO30'

2. Filter by vendor (Add Series to Project)

Only process Siemens data:

def filter(context):
    exam = context.get('series', {}).get('exam') or {}
    vendor = exam.get('vendor', '')
    return 'siemens' in vendor.lower()

3. Filter by acquisition parameter — repetition time (Add Series to Project)

Only process series with a TR greater than 2000 ms. Parameters are found inside each dataset of the series:

def filter(context):
    for dataset in context.get('series', {}).get('datasets', []):
        for param in dataset.get('parameters', []):
            if param['Name'] == 'RepetitionTime':
                return param['Value'] > 2000
    return False

4. Filter by protocol name (Add Series to Project)

Only process series whose protocol name contains “fMRI”:

def filter(context):
    for dataset in context.get('series', {}).get('datasets', []):
        for param in dataset.get('parameters', []):
            if param['Name'] == 'ProtocolName':
                return 'fMRI' in str(param['Value'])
    return False

5. Filter by series name pattern (Add Series to Project)

Only process series whose name starts with “t1” (case-insensitive):

def filter(context):
    series = context.get('series') or {}
    return series.get('name', '').lower().startswith('t1')

6. Filter by patient sex (Add Study to Project)

Only process studies for female patients. The patient is accessed via exam.patient:

def filter(context):
    patient = context.get('exam', {}).get('patient') or {}
    return patient.get('sex') == 'f'

7. Filter by scanner and TR combined (Add Series to Project)

Only process Siemens series with a TR above 1000 ms:

def filter(context):
    series = context.get('series') or {}
    exam = series.get('exam') or {}

    vendor = exam.get('vendor', '').lower()
    if 'siemens' not in vendor:
        return False

    for dataset in series.get('datasets', []):
        for param in dataset.get('parameters', []):
            if param['Name'] == 'RepetitionTime':
                return param['Value'] > 1000
    return False

8. Filter using a numeric DICOM tag (Add Series to Project)

Access parameters by their (group,element) tag. This example uses (0008,103e) (Series Description) and (0018,0081) (Echo Time):

def filter(context):
    for dataset in context.get('series', {}).get('datasets', []):
        series_desc = ''
        te = None
        for param in dataset.get('parameters', []):
            if param['Name'] == '(0008,103e)':
                series_desc = str(param['Value'])
            if param['Name'] == '(0018,0081)':
                te = param['Value']

        if 'localizer' in series_desc.lower():
            return False
        if te is not None:
            return te < 30
    return False

9. Check study completeness (Add Study to Project, with delay)

Fire only when all required series have arrived. Use the Add Study to Project event so that the action is applied to the study rather than to an individual series. Configure a delay long enough for all series to be imported (e.g. 10–30 minutes depending on your scanner workflow).

When a delay is configured the filter is evaluated after the delay, using a freshly fetched study that includes all series imported in the meantime. context['exam'] therefore reflects the full state of the study at that point. Pair this filter with Add Tag or Mark valid — both are idempotent if the trigger fires more than once.

def filter(context):
    required = {'T1', 'T2', 'FLAIR'}
    exam = context.get('exam') or {}
    current = {s.get('name', '') for s in exam.get('series', [])}
    return required.issubset(current)

For case-insensitive matching or partial name checks:

def filter(context):
    required = {'t1', 't2', 'flair'}
    exam = context.get('exam') or {}
    current = {s.get('name', '').lower() for s in exam.get('series', [])}
    return required.issubset(current)

10. Filter datasets by type (Add Dataset to Project)

Only process NIfTI datasets:

def filter(context):
    dataset = context.get('dataset') or {}
    return dataset.get('type') == 'nifti'

11. Filter dataset by acquisition parameter (Add Dataset to Project)

Parameters are directly on the dataset for this event:

def filter(context):
    for param in context.get('dataset', {}).get('parameters', []):
        if param['Name'] == 'RepetitionTime':
            return param['Value'] > 2000
    return False

11. Fire only on weekdays (Periodic)

Use context['timestamp'] to gate execution by day of week:

def filter(context: dict) -> bool:
    from datetime import datetime
    dt = datetime.fromisoformat(context['timestamp']['iso'])
    return dt.weekday() < 5   # Monday = 0, Friday = 4

12. Skip if last run was recent (Periodic)

Prevent back-to-back runs when the trigger interval is shorter than the desired minimum gap:

def filter(context: dict) -> bool:
    from datetime import datetime, timedelta, timezone
    last_run = context.get('last_run')
    if last_run is None:
        return True   # first run, always proceed
    elapsed = datetime.now(timezone.utc) - datetime.fromisoformat(last_run)
    return elapsed >= timedelta(days=7)

12.5. Actions

When a trigger fires and the selector matches, one of the following actions is executed:

Action

Description

Link to folder

Creates a link to the matched object inside the specified folder path. The path can be defined as a Jinja2 template or a Python script (see below).

Copy to project

Copies the matched objects to another project, optionally into a specific folder path. The path can be defined as a Jinja2 template or a Python script.

Move to project

Moves the matched objects to another project, optionally into a specific folder path. The original object is removed from the source project after the move. The path can be defined as a Jinja2 template or a Python script.

Run task

Starts a configured task with the matched objects as inputs.

Rename

Renames the matched object. The new name can be defined as a Jinja2 template or a Python script.

Add Tag

Applies one or more existing tags to the matched object.

Remove Tag

Removes one or more tags from the matched object. Only tags that are currently applied to the object are affected; missing tags are silently ignored.

Send Email

Sends an email to one or more project members. Subject and body are Jinja2 templates rendered with metadata from the triggering object.

Log Message

Writes a custom message to the project timeline and the server log file. The message is a Jinja2 template rendered with metadata from the triggering object.

Python Script

Executes a Python run(context) function that can perform multiple operations in a single action: rename, create folders, link/move the object, copy to another project, and apply tags. All operations are optional.

Note

Periodic triggers: restricted action types

Because a Periodic trigger fires on a schedule with no triggering object, actions that operate on a specific object (Link to folder, Copy to project, Rename, Add Tag) are not available. Only the following actions can be selected for a Periodic trigger:

  • Run Task — runs a task that requires no object inputs.

  • Send Email — sends a notification email to project members.

  • Log Message — logs a message to the timeline and server log on each scheduled run.

  • Python Script — the most flexible option; can create folder structures and use project/time/exam-tree context. See Python Script for Periodic triggers below.

12.5.2. Run task

The Run task action starts a configured task with the matched objects as inputs. Select the task to run from the dropdown. The task is launched immediately when the trigger fires.

12.5.3. Rename

The Rename action changes the name of the matched object. The new name is specified as a Jinja2 template or a Python script, using the same expression modes described in Folder path / Name template above.

For Python script mode, define a build_name function instead of build_path:

def build_name(context: dict) -> str:
    series = context.get('series') or {}
    exam = series.get('exam') or {}
    date = (exam.get('start_time') or '')[:10].replace('-', '')
    return f"{date}_{series.get('name', 'series')}"

Rename a series to include the acquisition date using a template (Add Series to Project):

{{ series.name }}_{{ exam.start_time.strftime('%Y%m%d') }}

12.5.4. Add Tag

The Add Tag action applies one or more existing tags to the matched object. Tags must already exist in the project (or as global tags). Select the desired tags from the multi-select dropdown.

The tags are applied to the primary matched object — the study for Add Study to Project, the series for Add Series to Project, or the dataset for Add Dataset to Project.

12.5.5. Remove Tag

The Remove Tag action removes one or more tags from the matched object. Select the tags to remove from the multi-select dropdown. Tags that are not currently applied to the object are silently ignored.

Like Add Tag, the action targets the primary matched object — the study, series, or dataset depending on the event type. A common pattern is to pair a Tag Added trigger with a Remove Tag action to replace one tag with another automatically.

12.5.6. Log Message

The Log Message action writes a user-defined message to the project timeline and the server log file each time the trigger fires. It is useful for annotating the timeline with domain-specific wording, debugging trigger chains, or creating an audit trail of events.

The message is entered as a Jinja2 template and rendered with the same context variables available to Rename and Send Email actions: exam, series, patient, and user. The rendered text appears as the timeline entry for the trigger run.

Example:

Study {{ exam.name }} arrived for patient {{ patient.patient_id }}

If the message field is left empty, the trigger name is used as the message.

12.5.7. Send Email

The Send Email action sends an email to one or more project members when the trigger fires.

12.5.7.1. Configuration

  • Recipients — select one or more project members from the dropdown. Only users with a configured email address will receive the message.

  • Subject — a Jinja2 template for the email subject line.

  • Body — a Jinja2 template for the email body.

The subject and body templates use the same variables as the other Jinja2 template fields (see Folder path / Name template above). The variable panel in the UI shows the available variables for the selected event type under Email Variables.

Email sending is fire-and-forget — a delivery failure (e.g. SMTP error) is logged but does not cause the trigger to fail or retry.

Note

Sending emails requires an email server to be configured in the Agora administration settings. If no email backend is configured, the action has no effect.

Example subjects and bodies (Add Series to Project)

Subject: New series arrived: {{ series.name }}
Body:    Study {{ exam.name }} ({{ patient.patient_id }}) has a new series "{{ series.name }}" in project {{ project.name }}.

12.5.8. Python Script

The Python Script action lets a single trigger perform several operations at once by returning a dictionary from a run(context) function. This is the most powerful action type — useful when you need to rename an object, organise it into a folder structure, and apply tags all in one step.

12.5.8.1. Script structure

The script must define a function named run that accepts a context dict and returns a dict:

def run(context: dict) -> dict:
    exam = context.get('exam')        # dict or None
    series = context.get('series')    # dict or None
    datasets = context.get('datasets')  # list of dicts or []

    name = (exam or series or {}).get('name', 'unnamed')
    return {
        # All keys are optional — include only what you need.
        'valid': True,                        # bool | None — True/False marks valid/invalid (green ✓ / red ✗); None clears the flag
        'rename': name,                       # str | None  — new name for the object
        'folders': [                          # list[str]   — folder paths to create
            f'{name}/raw',
            f'{name}/processed',
        ],
        'link_to': f'{name}/raw',             # str | None  — link/move the object here
        'tags': ['my-tag', 'another-tag'],    # list[str]   — apply tags by label
        'copy_to_project': {                  # dict | None — copy to another project
            'project': 'OtherProject',        # target project name
            'path': 'incoming',               # target path (optional)
        },
    }

All dictionary keys are optional. Operations are applied in this order: set valid → rename → create folders → link/move → copy to project → apply tags.

12.5.8.2. Return value keys

Key

Type

Description

valid

bool or None

Marks the triggering object (exam or series) as valid (True) or invalid (False). Shows a green ✓ or red ✗ icon in the exam list and detail views. Ignored for periodic triggers (no triggering object). Omit or set to None to leave the current state unchanged.

rename

str or None

New name for the matched object. Skipped if None or absent.

folders

list[str]

List of folder paths to create relative to the project root. Folders are created if they do not already exist.

link_to

str or None

Path of an existing (or just-created) folder to link the object into, relative to the project root. Free datasets (with no parent series) are moved rather than linked.

tags

list[str]

Tag labels to apply to the object. Tags are looked up by label; if a tag does not exist it is created as a project-scoped tag.

copy_to_project

dict or None

Copy the object to another project. Must contain 'project' (target project name). 'path' is optional.

The context dictionary passed to the run function has the same structure as described in The context dictionary, with one addition: context['datasets'] is always a list (empty if no datasets are associated), and context['folder'] may be set for folder-based events. Project counters are also injected into the context — see Project Counters.

Example — rename, create folders, and link (Add Series to Project)

def run(context: dict) -> dict:
    exam = context.get('exam') or {}
    series = context.get('series') or {}
    patient = exam.get('patient') or {}

    pid = patient.get('patient_id', 'unknown')
    exam_name = exam.get('name', 'unnamed')
    series_name = series.get('name', 'series')

    return {
        'rename': f'{exam_name}_{series_name}',
        'folders': [
            f'{pid}/{exam_name}/raw',
            f'{pid}/{exam_name}/processed',
        ],
        'link_to': f'{pid}/{exam_name}/raw',
    }

Example — tag by scanner vendor (Add Series to Project)

def run(context: dict) -> dict:
    exam = context.get('series', {}).get('exam') or {}
    vendor = exam.get('vendor', '').lower()
    tag = 'siemens' if 'siemens' in vendor else 'other-vendor'
    return {'tags': [tag]}

12.5.8.3. Python Script for Periodic triggers

For Periodic triggers the script receives project and schedule metadata instead of an object. Only the folders key in the return dict is applied — valid, rename, link_to, copy_to_project, and tags are silently ignored because there is no triggering object to act on.

The context contains:

Key

Description

context['project']

{'id', 'name', 'description'} — the project the trigger belongs to.

context['timestamp']

{'year', 'month', 'day', 'hour', 'minute', 'iso'} — current run time.

context['last_run']

ISO datetime string of the previous execution, or None on first run.

context['exams']

Full exam tree for the project (lazy-loaded only when the word exams appears in the script outside of comments).

Example — create a dated folder each time the trigger fires

def run(context: dict) -> dict:
    ts = context.get('timestamp', {})
    return {
        'folders': [f"reports/{ts['year']}/{ts['month']:02d}"],
    }

Example — create a folder for each exam in the project (uses lazy exam tree)

def run(context: dict) -> dict:
    folders = []
    for exam in context.get('exams') or []:
        name = exam.get('name', 'unnamed')
        folders.append(f'{name}/raw')
        folders.append(f'{name}/processed')
    return {'folders': folders}

Example — skip execution outside business hours

Combine a Periodic filter script (see periodic filter examples above) with this action, or guard inside the action itself:

def run(context: dict) -> dict:
    ts = context.get('timestamp', {})
    hour = ts.get('hour', 0)
    if not (8 <= hour < 18):
        return {}   # nothing to do outside 08:00–18:00
    return {'folders': [f"daily/{ts['year']}-{ts['month']:02d}-{ts['day']:02d}"]}

12.6. Project Counters

Project counters are project-scoped integers that increment automatically each time a trigger references them. They are available in all action types that support expressions — Jinja2 templates and Python scripts alike — and are useful for assigning sequential numbers to imported objects without manual bookkeeping.

Counters are created and managed in the project settings under Counters. The variable panel in the trigger editor lists all counters that exist for the current project.

12.6.1. Using counters in Jinja2 templates

Use the counter name directly as a template variable. Each time the template is rendered the counter’s current value is returned and the value in the database is incremented by 1:

Studies/Study_{{ counter1 }}
{# → "Studies/Study_1" on first use, "Studies/Study_2" on second, … #}

Combining a counter with other metadata:

{{ exam.scanner_name }}/{{ exam.start_time.strftime('%Y') }}/study_{{ counter1 }}
{# → "GYRO30/2024/study_1", "GYRO30/2024/study_2", … #}

12.6.2. Using counters in Python scripts

In Python scripts (build_path, build_name, and run), project counters appear as keys in the context dictionary. Each counter is a Counter object that wraps the current integer value.

The Counter object behaves like an integer in arithmetic, comparisons, and string formatting:

Operation

Result

int(context['counter1'])

Current integer value.

str(context['counter1'])

String representation of the current value (e.g. "3").

f"study_{context['counter1']}"

Formatted string using the current value.

context['counter1'].increment()

Increases the value by 1. The database is updated after the script returns.

context['counter1'].increment(by=N)

Increases the value by N.

Call .increment() to schedule the counter to advance. The actual database update is applied atomically after the script completes using a SELECT FOR UPDATE lock, so concurrent trigger executions always receive distinct values.

Note

The counter value in the context reflects the state at the time the trigger fires. Calling .increment() schedules a delta to be applied to the database after the script returns — it does not write to the database mid-script.

Example 1 — sequential study numbering (Add Study to Project, Python Script action)

Rename each incoming study with a sequential number and place it in a matching folder:

def run(context: dict) -> dict:
    exam = context.get('exam') or {}
    scanner = exam.get('scanner_name', 'scanner')

    n = context.get('counter1')        # Counter object, e.g. current value = 3
    label = f'Study_{n}'               # "Study_3"
    n.increment()                      # database will be updated to 4 after script returns

    return {
        'rename': label,
        'folders': [f'{scanner}/{label}'],
        'link_to': f'{scanner}/{label}',
    }

Example 2 — sequential numbering in build_path (Add Series to Project, Link to folder)

Organise series by scanner with a per-study counter in the path:

def build_path(context: dict) -> str:
    series = context.get('series') or {}
    exam = series.get('exam') or {}
    scanner = exam.get('scanner_name', 'unknown')

    n = context.get('counter1')
    path = f'{scanner}/study_{n}/{series.get("name", "series")}'
    n.increment()
    return path

Example 3 — two counters, different meanings (Add Series to Project, Python Script action)

Use one counter for the study number and a second for a global scan count across all studies:

def run(context: dict) -> dict:
    series = context.get('series') or {}
    exam = series.get('exam') or {}

    study_no = context.get('counter1')   # e.g. counts studies: 1, 2, 3, …
    scan_no  = context.get('counter2')   # e.g. counts all scans globally

    label = f'Study{study_no}_Scan{scan_no}'
    study_no.increment()
    scan_no.increment()

    return {
        'rename': label,
        'link_to': f'archive/study_{study_no}',
    }

Example 4 — increment by more than 1

If each study spans multiple series and you want the counter to advance by the number of series in the study:

def run(context: dict) -> dict:
    exam = context.get('exam') or {}
    series_count = len(exam.get('series', []))

    n = context.get('counter1')
    label = f'batch_{n}'
    n.increment(by=series_count)   # advance by the actual series count

    return {'rename': label}

12.7. Developing and Debugging Python Scripts

Writing a trigger script without being able to inspect the context it will receive is tedious. Agora provides context endpoints that return the exact JSON dict a script would see for any existing object, so you can develop and test offline before deploying the trigger.

Step 1 — fetch the context for a real object

While logged in to Agora, open the following URL in your browser (replace the ids with real values):

# Exam context
/api/v2/project/<project_id>/exam/<exam_id>/trigger_context/

# Series context
/api/v2/project/<project_id>/series/<series_id>/trigger_context/

# Dataset context
/api/v2/project/<project_id>/dataset/<dataset_id>/trigger_context/

# Folder context
/api/v2/project/<project_id>/folder/<folder_id>/trigger_context/

The browser will display the JSON response. Copy it and save it as context.json.

Add ?include_parameters=true to include the full DICOM parameter list for each dataset — useful when your script reads acquisition parameters.

Step 2 — run your script locally

Load the saved JSON and call your function directly:

import json

with open("context.json") as f:
    context = json.load(f)

# paste your trigger function here, then call it:
def run(context: dict) -> dict:
    exam = context.get("exam") or {}
    return {"link_to": exam.get("name", "unknown")}

print(run(context))

Iterate until the output is correct, then paste the final function into the trigger script editor.

Note on counters: context.get('my_counter') returns None when running locally because counter objects are injected at runtime by Agora. Guard with if n := context.get('my_counter') or provide a fallback integer when testing offline.


12.8. Trigger History

Each trigger execution is recorded in the project Timeline. Open the Timeline from the project navigation to see when each trigger last ran, which objects it matched, and whether the action succeeded.