fix(workflows): rebuild the library templates against what the core actually has - #72
Conversation
…tered
Every predicate and the write itself were fiction against the core:
- `paymentMethod`, `paymentStatus` and `maxAgeDays` are not filters on
SalesOrder, and `status: "open"` is not one of its options (draft / confirmed /
fulfilled / closed / cancelled). In the flat shape none of them filtered
anything, so the list came back as the WHOLE order book.
- `status` is `access: readOnly`. The old `body: {status: "cancelled"}` could
never have cancelled anything — cancelling is the `cancel` process step.
- the only guard asked whether the order was ALREADY cancelled, which does not
protect against a row that was never supposed to be in the list.
Rebuilt against what the core has, all measured on mvp:
- `status` and `dates.issued` filter server-side in the bracketed shape. The
operator is `lessThan`, not `lt` — upstream answers 400 and names the eight it
accepts. A live run caught that; the static check could not.
- `payment.method` comes back as `{id, href}` with no name, so the method is
resolved through the PaymentMethod catalogue (one call, `Vorkasse` -> paym_21).
- `payment.status` is null on a list, so "still unpaid" is read off
`trafficLights`, where it is present.
- cancelling goes through the `cancel` process step.
Measured effect on mvp: the server-side filters return 100 orders, none with the
wrong status and none newer than the cutoff, and the guard narrows those to 3 —
prepayment and unpaid. The old version would have looped the entire order book.
Same family of defect as prepayment_order_cleanup: invented filter keys and values, and — the conceptual one — a state transition modelled as a field write. `status` is readOnly on every document; releasing is an ACTION. auto_release_paid_orders `status: "paid_awaiting_release"` is not one of the options (draft / confirmed / fulfilled / closed / cancelled), and the old graph WROTE `status` twice, which could never have worked. "Paid" is not filterable on SalesOrder and `payment.status` is null on a list, so it is read off `trafficLights`, where it is present. Releasing now goes through the `release` step, and a second guard refuses to release an order that still carries a hold — the old version checked a `stockAvailable` field that does not exist. daily_opos_digest `paymentStatus: "open"` was wrong twice: the key is `payment.status` and its options are unpaid / partiallyPaid / paid. Both verified against mvp: `SalesOrder status=draft` returns 50 rows, all draft; `SalesInvoice payment.status=unpaid` returns 50, all unpaid; and of the 50 orders, 2 are paid and unheld — the ones the workflow would release. Two further rebuilds are finished but NOT shipped here, because the core cannot serve them yet (see the PR): filtering DeliveryNote by `shipped` and Return by `checked` returns records in a different state.
Both were rebuilt earlier and held back because the core returned records in a different state than the filter asked for. agent-cores #89 fixed that, so they ship now. return_to_credit_note `SalesReturn` does not exist — a read of it answers 404. The entity is `Return`. It filtered `creditNoteCreated`, which is not a field; the readable `resolution.creditNote` decides that on the record instead. And the credit note does not have to be assembled from `customerId`/`positions` (neither exists on the model): `Return` carries a `createCreditNote` action. The lifecycle filter is `progress`, not `status` — `status` merges in `cancelled` and cannot answer `checked` (agent-cores #89). invoice_after_shipment `invoiced` is not a filter; `documents.salesInvoices` is readable, so "not yet invoiced" is decided on the record. The invoice is not assembled by hand either — DeliveryNote carries `createSalesInvoice`. Both now do the same thing the whole set was missing: they ask the ERP to perform the transition instead of writing a status field that is readOnly. Server-side filters verified against mvp: `Return progress=checked` returns 7 records, all checked (it used to return 32 that were all settled), and `DeliveryNote status=shipped` now resolves to upstream `sent` and returns 50, all sent (it used to return 1, and that one read as delivered). Requires a CORES_VERSION carrying agent-cores #89. Without it the two filters return the wrong state — which is exactly why these two waited.
`belowMinStock: true` is not a field on Product. In the flat shape it filtered
nothing, so the workflow proposed purchase orders for the whole catalogue.
The real constraint is that `stock` is a detail-only section: a product LIST
always returns it null, so "below minimum" cannot be decided from a list at all.
What IS on the list is `logistics.minimumStockQuantity` and `suppliers` — so the
loop spends a `StockLevel` call only on articles that carry a minimum.
Measured on mvp: 100 active products in one call, 10 of them carry a minimum, so
the per-article call is paid 10 times rather than 100. All 10 are below minimum
and NONE has a default supplier, so the run ends with zero purchase orders and
ten records on the "below minimum, no supplier" branch.
That branch is the point. A purchase order needs a supplier; without one the old
shape would have produced an order with no recipient, or dropped the article
silently. Now it is visible.
Also fixed while building: the loop referenced `{{ artikel_liste }}`, and the
renderer derives `artikelListe` — camelCase, not snake_case. The list resolved to
nothing, the loop skipped, and its whole body (including the StockLevel read)
never rendered. The renderer says so itself in the skip message; the static
validator reports it as `loop_items_unresolved`.
|
Was auf der Liste steht, ist Live auf mvp gemessen: Der letzte Zweig ist der Punkt: eine Bestellung braucht einen Lieferanten. Ohne ihn hätte die alte Form eine Bestellung ohne Empfänger erzeugt oder den Artikel still fallengelassen. Jetzt ist er sichtbar. Beim Bauen noch eine Namensfalle gefunden: die Schleife referenzierte |
I had recommended withdrawing this template, on the grounds that dunning has no
public API. That was wrong — `dunningSettings` sits in `UpdateInvoiceData` and
the level is writable (agent-cores #90). The old version was still fiction: it
filtered `paymentStatus` and `minOverdueDays`, neither of which is a filter, and
wrote `body: {dunningLevel: 3}`, which is not a field.
Rebuilt against what the core has:
- `payment.status = unpaid` plus `dates.issued lessThan today(-14)` server-side.
- Due date is computed from the document date and the invoice's own payment
terms, because `payment.dueDate` comes back EMPTY on a list — measured, null on
all 100 unpaid invoices on mvp. `dates.issued` and `payment.terms.dueDays` are
both there.
- The level walks the upstream ladder (paymentReminder1 -> reminder1..3 ->
debtCollection -> lossOfReceivables) and stops at the top instead of writing
past it.
- A dunning block is honoured, and every skip says why in the run log.
The write is `body: {dunning: <ref>}` — one whole object bound at the TOP level,
because a `{mode: ref}` nested deeper inside a body is not resolved and would
travel to the ERP verbatim.
The date maths uses the runtime's `days_since` helper: a plain
`from datetime import ...` in a code box is refused by the AST allowlist
(`unsafe_code`), which the static validator caught before this ever ran.
Measured on mvp: 100 unpaid invoices issued before the cutoff, all on
`reminder3`, all overdue — every one would escalate to `debtCollection`, none is
blocked or already at the top.
Both descriptions now say what the workflow does NOT do: it records the level, it
does not SEND a reminder. No endpoint does.
Needs a CORES_VERSION carrying agent-cores #90.
Four templates rebuilt. They were not "broken filters" — they were written against an API that does not exist.
All 12 workflow templates were created on 2026-06-27 in one commit ("Populate library catalogue (246 items)"), without ever running against a core.
init_from_templatecopies a graph verbatim, so a fictional template becomes a fictional workflow on a customer instance.The conceptual defect under most of it
They model state transitions as field writes —
body: {status: "cancelled"},{status: "released"}.statusisreadOnlyon every document. Cancelling, releasing and invoicing are actions. Almost everything else follows from that.What changed
auto_release_paid_orders—status: "paid_awaiting_release"is not one of the options, and the graph wrotestatustwice. Now:status=draftserver-side, "paid" read offtrafficLights(payment.statusis null on a list), release through thereleasestep. Plus a guard that did not exist: an order still carrying a hold is not released — the old version checked astockAvailablefield that does not exist.prepayment_order_cleanup— the dangerous one. Four flat keys, none of them filters, then a loop that cancelled everything whose status was not alreadycancelled. Its only guard could not protect against a row that was never supposed to be in the list. Now:status=draft+dates.issued lessThan today(-7)server-side, payment method resolved through the PaymentMethod catalogue (payment.methodcomes back as{id, href}with no name), unpaid read offtrafficLights, cancelling through thecancelstep.daily_opos_digest—paymentStatus: "open"was wrong twice: the key ispayment.status, the options areunpaid | partiallyPaid | paid.return_to_credit_note—SalesReturnanswers 404; the entity isReturn.creditNoteCreatedis not a field —resolution.creditNotedecides it on the record. The credit note is not assembled fromcustomerId/positions(neither exists):ReturncarriescreateCreditNote. Filters onprogress, notstatus.invoice_after_shipment—invoicedis not a filter;documents.salesInvoicesis. The invoice comes from DeliveryNote'screateSalesInvoice.Measured on mvp
auto_releasereleasableReturn progress=checkedsettledDeliveryNote status=shippeddeliveredsentSalesInvoice payment.status=unpaidvalidate_graph, the render andcheck_workfloware clean in both locales for all four.Requires
A
CORES_VERSIONcarrying agent-cores #89 —return_to_credit_noteandinvoice_after_shipmentfilter on states the core only resolves correctly after it. That is exactly why those two waited.Not in here
opos_dunning_escalationis not buildable.dunning.levelisreadOnlyand theremindaction carries the wish "Dunning has no public API". The template promises something the platform cannot do — it should be withdrawn, not repaired.low_stock_reorder_proposalneeds a decision:stockis detail-only, so it is always null on a product list. Either oneStockLevelcall per article, or withdraw it.