An automation can be triggered two ways, and they are stored in different
places: global triggers live in the automation settings, event triggers live
on the event itself, inside the rundown. The panel only ever receives the
first list.
So the Runs on column was reading an automation attached to ten events as
having no triggers at all, and saying "Never runs" about something that runs
perfectly well. Worse than a missing hint: it taught that event triggers do
not count.
The client cannot honestly answer the question either, since it holds only
the rundown that is currently loaded and a project can have several. So the
column no longer answers it. An empty cell now reads as an em dash, the same
as the filter rule column beside it, and claims nothing.
The "No outputs" tag stays. An automation with nothing to send does nothing
whatever triggers it, and that the panel can see for itself.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AfDKsy6PE3Rbyt32Fg4YKf
The automation form opens in the wide modal, whose body sets overflow hidden
because a wide modal is expected to manage its own scrolling. This one did
not, so the form was simply clipped: with five outputs the content ran to
2004px inside a 590px box and both the last outputs and Save were out of
reach, with nothing on screen to say so.
The form now owns its scrolling through the shared ScrollArea, and so does
the recipe list, which caps rather than fixes its height so the dialog still
shrinks to two rows when a search narrows it. The search moves out of the
scrolling region, which retires the sticky positioning that stood in for it.
The recipe section for Ontime's own actions was named after a property,
'Works out of the box', while every other section is named after what it
talks to. It is now 'Ontime automations' and carries the actions that were
missing: stopping an aux timer on finish, and pointing the stage timer's
secondary field at one.
Those need to know which of the three aux timers you mean, which is a choice
rather than something to type, so a parameter can now declare options and
render as a select. Action keys are a union the compiler checks against the
schema, so the aux number resolves through maps rather than string
interpolation: an unexpected value falls back to the first timer instead of
building an action the server would reject.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AfDKsy6PE3Rbyt32Fg4YKf
A recipe knows almost everything about the automation it makes. What it cannot
know is where your gear is: which machine runs QLab, which Companion button,
how long the aux timer should run. Handing the user the full automation form
to supply three of those made them read a form of twenty fields to change one.
A recipe now declares its parameters, and picking one asks for those and
nothing else. Create makes the automation and its trigger through the same
endpoints the form uses, so what lands in the list is an ordinary automation
with nothing special about it.
Each recipe carries a `build` function rather than a literal, which is what
lets the answers reach the outputs — a Companion address and a page, row and
column become one URL. That also absorbs the two things a user does without
thinking: an address pasted with a trailing slash, and a webhook URL that
already carries a query string. Every default still points at loopback.
For the list to hold many recipes it has to be searchable and grouped, so it
is both. Search matches the title, the description, the category and a
keywords list, so the Companion recipe answers to "stream deck" and "elgato"
and QLab answers to "osc" and "audio". Enter takes the top result. Escape
clears the search rather than closing the dialog, which is the behaviour the
settings search already has.
The picker and the parameter step are two views of one dialog rather than two
stacked modals, so Back means back rather than dismissing everything. Rows
carry only the lifecycle tag: with many recipes, the description and one tag
is what stays readable.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AfDKsy6PE3Rbyt32Fg4YKf
AutomationForm rendered all three output kinds inline, so the file mixed the
form's own concerns with the fields of every protocol it can speak. Each kind
now has a component next to OntimeActionForm, which was already there and was
the odd one out, and OutputCard owns the chrome they share. The form drops
from ~700 lines to ~540 and the map over the outputs reads as three cases.
Three copies of an inline structural cast existed only to reach an output
field's error by name, which react-hook-form cannot resolve through a union.
One OutputErrors type replaces all of them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AfDKsy6PE3Rbyt32Fg4YKf
Making an automation took two visits to two lists. You wrote the automation
in one, then remembered that an automation alone never runs, went to the
trigger list, made a trigger, and pointed it back at what you had just made.
A first time user who stopped after the first step got a thing that looked
finished and did nothing.
Lifecycles are now picked on the automation form, as chips. Saving reconciles
the triggers behind it, diffed against a snapshot taken when the form opened
so a save never removes a trigger the user could not see. Every lifecycle is
in one place, including the two that fire continuously, which say so before
you pick them rather than after your log fills up.
That leaves the trigger list as what it actually is: a place to rename a
generated trigger, or to point several differently named ones at the same
automation. It says so, and it names the triggers whose automation is gone.
New opens one list of starting points: an empty automation, then a handful of
recipes for the software people actually pair Ontime with. A recipe is
nothing but a pre-filled form, so choosing one opens the ordinary automation
form with its values in place. The user reads what it will do, points it at
their own gear and saves. Nothing is written until they do, and there is no
second creation path to keep working: one request, the same as any other
automation. Every recipe targets loopback, so one saved without thinking
cannot put traffic on a venue network.
The list now answers what an automation does without opening it: when it
runs, whether it filters, and what it sends. An automation with no trigger or
no output is the common half-finished state and is called out rather than
shown as a blank cell. Outputs are cards with their type, a summary and a
Test button in the header, which is also where the test says whether it
worked: the panel used to send and tell the user nothing either way.
Deleting confirms first and names the triggers that go with it, instead of
dumping the server's refusal into a stray row under the table.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AfDKsy6PE3Rbyt32Fg4YKf
The server refused to delete an automation while any global trigger pointed
at it, so removing one meant finding its triggers in a second list and
deleting each by hand first.
The refusal was protecting against dead data, but a trigger pointing at a
deleted automation is the dead data. Both are now written in a single patch,
so there is no window where one outlives the other and nothing to roll back.
An automation attached to an event is still refused: that reference lives in
the rundown, and removing it is an edit to the show rather than to the
automation settings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AfDKsy6PE3Rbyt32Fg4YKf
A lifecycle was written three ways depending on where you were standing:
"On Start" in the automation settings, the raw "onStart" in the event editor,
and again as a literal in the trigger list. Anyone attaching an automation to
an event had to work out that the two lists were the same list.
One label map now owns the user facing names, and one helper summarises what
an automation sends. Both are shared, so the event editor gains the labels and
a Sends column for free: the trigger row says which automation runs and what
it will do, instead of only its title.
Two things the panel could not say before, now that it can look them up:
- A trigger can outlive the automation it points at, say after a partial
project import. It rendered as an empty tag; it now says so.
- not_contains is in the type and the runtime, but the server validation list
omits it, so an automation using it cannot be saved. The operator list moves
out of the form and drops it, with a test to stop it coming back.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AfDKsy6PE3Rbyt32Fg4YKf
Search every text field with optional filters, keep result selection stable, and expose clearer match context and counts. Cover query parsing, matching, mixed-entry indexing, and the reveal flow.