Re-read after the review pass · 31 Aug 2026
Sixteen acceptance criteria now, nine of them marked Reviewed, checked against what
fmx-automations has and what its verification ledger says has actually run. The
review pass ratified the architecture and opened two new engine gaps — and
renamed Iris to "an LLM" throughout, which is a bigger signal than it looks.
Where the sixteen land
Met or near — built and exercised against a real record
Partial — the mechanism exists, the criterion's full surface does not
Blocked — built, cannot be proven; waiting on an FMX right or setting
Not started — nothing in the codebase reaches it
New criterion · already met
"Side-car, doesn't need to be in core app, can be turned on for select customers." That is
the premise fmx-automations was built on and the README spends pages
defending. Per-tenant webhooks, per-site enablement, its own deploy train: all there.
Nothing to build. Stop arguing the architecture in future write-ups.
New gap · cheap now
Status became a branchable field
Request Property Branches gained Status as item #1. It is not in the
engine's READABLE list, and after 6e136d3 the validator now
refuses a plan naming a field the engine never asked FMX for. So the review created
a requirement the engine actively rejects rather than silently mis-answers.
Three touchpoints, per that commit's own rule: the readable list, the sweep's candidate query, and one run against a real record.
Open question · answer before more plans exist
Configurable Workflow Steps adds: "TBD if multiple triggers can be assigned to the same workflow (for example, if a request being created or edited triggers the same auto-assignment flow)."
Plan.trigger is a single object. If that resolves to yes it is a stored-shape
change, and it gets more expensive with every plan written in the meantime.
New requirement
"It should be easy to visualize and communicate the overall workflow from FMX. This could be through using a visual workflow diagram or something else."
Lands on the same missing surface as branch authoring — the builder cannot author a branch and now has to draw one too. One piece of work, not two.
Risk retired
Unified Workflow Location now records it explicitly, and names the GA fix rather than treating it as a defect.
For now, the side-car approach means the workflow will only be able to do things that the creating user can do… This is acceptable for the MVP but will need to be corrected in the final GA version. Details, Unified Workflow Location
The engine already surfaces the failure — an automation whose owner is gone lands in the "cannot run" group. A reviewer's comment names a service account as the GA direction.
Confirmed by the PM
A comment now sits on the Module line of Edit Action: "Missing the API end-point for
this." Same conclusion the catalogue reached when it withheld
action.record.switchModule.
Converts an engine shortfall into an API request. Pair it with
action.record.notify, which has the identical problem, and propose both as
formal endpoints together.
Scope may shrink
LLM-Performed Actions adds: "Could we have the LLM create a function to act reliably each time based on LLM created rules, rather than being judgement based each run?"
That is authoring-time generation, not a per-run model call — and it converges with LLM-Built Workflows and Custom Javascript. If it lands there, three criteria become one build the validator already half-supports.
Bar raised
The criterion was edited to require a work request being fully approved — exactly the caveat the verification ledger already carries about the multi-tier rule changing underneath the old C# evidence.
No new build. But the blocked row cannot be closed by taking a single-tier request through; it needs a real multi-tier approval line.
What exists
The entire app. A standalone Next deployment on Vercel reaching FMX only through the public API, with its own store, its own deploy train, one webhook per organization and per-site enablement.
Gap
None. This criterion describes what was already built, which is worth saying out loud: it is the one place the engine is ahead by an entire architecture.
What exists
One list at /, grouped by running / off / cannot run, with a one-line summary
per card. Create, edit and delete are there — delete is a soft is_deleted, so
restore is a query away.
Gap
Still no restore, duplicate or search.
Moved in review: scope narrowed to "the new ones created in this new platform", so it need not unify with FMX's existing rules. The creating-user permission model is now accepted for MVP. New sub-question: should a duplicate land in draft rather than run immediately?
What exists
The engine meets the branching half in full. Step.branches nests to any depth,
a condition with branches is a router, otherwise is the Else the criterion
asks for, and the validator walks every branch — unique ids plan-wide, and an action may
not branch.
Gap
The builder never mentions branches. Nothing in builder.tsx or
step.tsx can author one, so authors get a flat condition-then-action list over
a data model that supports the whole thing.
Moved in review: a visual workflow diagram is now a requirement, and multiple
triggers per workflow is an open TBD that Plan.trigger cannot express.
What exists
trigger.record.fieldChanged, and the best-evidenced thing in the repo: a real
edit to request 7151902 reached the receiver and produced mode: Event,
outcome: Matched — twice on production, 137 times on development.
Gap
The criterion wants the author to name which properties fire it. The block takes
only entityType, so it fires on any edit and the narrowing has to be done
afterwards as conditions. Building Group is not among the six it lists that the engine can
read.
What exists
trigger.record.submitted, offered and validated. Never run:
POST /maintenance-requests refuses every building as invalid for this account,
across four attempts against a module reporting canCreate: true.
Gap
Needs the create right looked at, or somebody else making a request while an automation watches. Pending Details is not addressed anywhere, and Building Group is not a field the engine reads.
What exists
trigger.record.approved is offered. access.canApprove is false
throughout and the module has no approval line to take a request along.
Gap
The ledger calls this its weakest row: only ever exercised on the retired C# engine, never verified, and the multi-tier rule changed underneath it.
Moved in review: the criterion now says FULLY approved. The two documents agree on the hard part, and the bar for closing this row went up.
What exists
Conditions read Module, Request type, Building, Priority, Name, Due date and free-text location, plus your own custom fields by name — verified with both outcomes over three swept records.
Gap
Three of nine now missing: Status, Building Group, and Resource & Location as a resource id rather than free text. Plus the branching itself has no builder, per the row above.
Moved in review: Status was added as item #1 and went from eight items to nine.
Since 6e136d3 the validator refuses a plan naming it, so this one fails loudly
rather than quietly.
What exists
An id, and a deliberate absence. condition.llm.classify is held out of the
catalogue because a block that cannot run is worse than no block.
Gap
No classifier anywhere in the app, and nowhere to put author instructions or branch options. All of it.
Moved in review: renamed from Iris-Determined. The requirement is now written as "an LLM (branded to the user as Iris)" — a model call, not the Iris product.
What exists
Two blocks that map cleanly onto the criterion's add-versus-override:
action.record.assign refuses a request already assigned,
action.record.reassign replaces whoever is on it. One real unattended write —
engine_writes 731, request 7151901.
Gap
Only named users. Assigning by user type has no expression in either block — and user type is now named twice in the PRD, here and in the limitations example under LLM-Performed Actions.
What exists
action.record.edit, verified once through the event path. The builder offers
three fields — Name, Due date, Other location. The executor accepts four more, and
Priority has its own block.
Gap
Of nine, three are not reachable at all: Module, Scheduled Time, and Custom Field Values, which can be read but never written.
Moved in review: a comment on the Module line reads "Missing the API end-point for this" — the same conclusion the catalogue reached. That item is now an API request, not an engine shortfall.
What exists
Nothing.
Gap
Beyond the model call, the criterion's own open question — how a technician's current workload gets in front of the model — has no answer here. Every read today is one record plus authoring options.
Moved in review: a new note asks whether the LLM could generate a reliable function once instead of judging each run. That is a materially smaller and more testable build, and it points at the same place as Custom Javascript.
What exists
The half that matters underneath it: plans are plain JSON, the catalogue is data, and the validator returns per-step problems in prose. A generated plan has something to be checked against.
Gap
No prompt, no generation path, no entry point. Still the cheapest of the five untouched criteria, because the validator already does the hard part.
Moved in review: it should also accept attachments — a diagram the customer drew — as input. Pairs with the diagram requirement under Configurable Workflow Steps: the same picture, read in one direction and drawn in the other.
What exists
Ahead of the criterion. is_enabled defaults false, so a saved plan is a draft.
POST /api/plans/{id}/evaluate describes every action and writes nothing at any
control level. trigger.run.now tries a plan against the real backlog, and the
Notify level records what it would have done, permanently.
Gap
Only that no UI surfaces the dry run against a chosen past request — the endpoint takes one, the page does not offer it.
What exists
A run is recorded every time an automation is woken, including when it matched nothing, and the full step trace is shown rather than summarised. A delivery log sits above it, so "connected and discarding everything" is visible.
Gap
No aggregate counts, no success or failure percentage, no alerting on a failed run, and no per-model-step accounting — which is what the usage-pricing note actually depends on.
What exists
The loop half, by three separate guards: a five-hop depth limit, an
engine_writes row written before each call so FMX's event for our own
write is recognised, and a 20-hour per-record sweep cooldown.
Gap
Nothing orders overlapping automations and nothing resolves contradictory actions. Two plans on the same trigger both run, last write wins, and which one that is is undefined.
What exists
Nothing.
Gap
Still the one criterion that argues against the current design: the engine's whole safety story is that a plan is data, an action is a named block, and a write happens in exactly one file. Arbitrary JS is the opposite bet — and worth resolving alongside the note under LLM-Performed Actions, which reaches for the same escape hatch from the other side.
Submitted, completed and approved each need a right or a module setting this account does not have. Only Edit is proven, and Approval's bar just went up. No amount of engine work moves those rows.
Still the schedule risk to raise first, and now all three are marked Reviewed — so the criteria are settled while the means of demonstrating them is not.
LLM-Determined Branches, LLM-Performed Actions, LLM-Built Workflows, plus the per-step accounting inside Run History. The rename from Iris changes the framing, not the state: there is no classifier, no prompt path and no context-provisioning mechanism in the engine.
The one thing that did move is the hint that a generated function might satisfy LLM-Performed Actions. Worth pushing on before anybody scopes four separate model features.
trigger.date.reached produced 96 matched runs in one sweep, and
trigger.nothing.happened has both outcomes plus its coverage caveat demonstrated
on 47 real records. Neither appears anywhere in the sixteen.
The pitch defers the jobs they serve, so this remains real, proven engine the MVP does not claim credit for.
Notify / Approve / Auto, the proposal inbox, decided_by_user_id: none of the
sixteen ask for it. Draft-then-publish remains the PRD's only trust mechanism, and the review
pass did not add one.
The Unified Workflow Location note came close — it accepts the permission model — but says nothing about a human approving individual actions. Worth settling before it reads as unplanned work.
Proposals 269 and 270 are Applied with no matching engine_writes
row, despite apply() having recorded one before every call since 26 Aug.
engine_writes is the loop guard: a write without a row is a write FMX's own event
makes look like a person's.
Not a PRD gap, and unchanged by the review. It still undercuts the loop protection Workflow Load Ordering will be built on top of.