Replication Flow Rows β a guide with ready-made patterns¶
Generated by build/scriptor/gen_patterns_doc.js from build/scriptor/patterns.json and the compiler itself; checked by scripts/witness_scriptor_patterns.js. Every row below is a sample: copy it, change what the pattern says to change, compile it.
1. What a flow row is¶
A flow row is one line of data that says: this kind of record moves, in this direction, between these people, and this is the key that makes one record unique. You do not write code for it. The compiler reads the row, reads the dictionary (the Application Dictionary: tables, columns, references, windows), and works out the rest: which other tables the receiver must already hold, which columns the system computes by itself, and what is missing.
Three parts work together, and you only touch the first:
| Part | What it is | Who touches it |
|---|---|---|
| The row | the declaration, the pages below | you |
| The treatment | the component that knows how to carry out one kind of row (FACT, SEED, RULE, INSTRUMENT, LOCAL). The row names it; it is found by name in a registry, like a factory. | an engineer, only to add a new kind |
| The plan reader | what the Modeller or the ERP uses to ask "is this table shared?" | the application author |
2. The fields of a row¶
| Field | Plain meaning | Example |
|---|---|---|
name |
A label you choose. Other rows refer to it by this exact text. | F1 pos-sales-up |
treatment |
The kind of movement. FACT = a record moves. SEED = a value is published and fetched. RULE = a named process that sends another flow, started by a flow arriving or by a role on a schedule. INSTRUMENT = a signed code that carries value. LOCAL = this table never leaves its station. |
FACT |
role |
The dictionary role (a number) of the devices that send. A label such as BORROWER is fine where there is no dictionary role. On a RULE with no on: the role that runs the process. |
103 |
strategy |
Which dictionary replication strategy supplies the table types. 100 is the sample point-of-sale strategy. | 100 |
dir |
UP = device to head office. DOWN = head office to a device. BOTH = every member of a team, both ways (needs audience space). |
UP |
audience |
For a FACT: hub (the default, through head office) or space (a team shares it). For a SEED: addressed (one role) or public (anyone). |
space |
table |
The dictionary table whose records move, or a table you declare yourself (section 4, pattern P7). | C_OrderLine |
carries |
The columns that travel. Leave it out and the dictionary proposes columns (a starting point, never a plan). "*" means all of them. |
["M_Product_ID", "QtyEntered"] |
group |
What makes one record unique, so that a resend is recognised and a clash is noticed. | POSStation/Date/Order# |
schedule |
A label for when the device sends (or, on a RULE with a role, when that role runs the process). It is recorded in the plan. |
end-of-day |
from, to |
Free text for people reading the plan. | POS station, HQ |
on, process, emits |
For a RULE: the named process sends flow emits. Either flow on arriving starts it, or a role on a schedule does (give role and schedule and leave on out; a person or department runs it, as in the dictionary). on and emits must be names of other rows in the same file. |
Replenish Report |
processId |
For a RULE: the dictionary process number the process name stands for (a whole number, for people reading the plan). |
53267 |
guard |
For a SEED: daily = ask at most once a day; once = ask only until you hold a value. |
daily |
authorityRole, holderRole, account |
For an INSTRUMENT: who issues the code, who applies it, and what the balance is called. |
103, PHONE, loyalty id |
virtualTable, columns |
A table you describe yourself because the dictionary does not know it. Each column has name, cls, and optionally target, mand, values. |
see P7 |
cls |
What kind of column: KEY (identity), REF (points to another table, needs target), MEASURE (a number), DATE, FLAG (yes or no), CODE (a short code, may have values), TEXT, OTHER. |
MEASURE |
target |
For a REF column: the table it points to. |
AD_User |
mand |
The column must be present when the record is created. | true |
values |
For a CODE column: the list of allowed kinds. Adding a kind later is adding one value, not code. |
["GEOM_EXTRUDE", "GEOM_SWEEP"] |
3. I wantβ¦ β use this pattern¶
| I want⦠| Pattern |
|---|---|
| My branch records sales and sends them to head office at the end of the day. Head office checks stock and sends a replenishment back. | P1 β A branch sells, head office answers with stock |
| Head office publishes a price list; each branch picks it up when it next connects, once a day. | P2 β A price list pushed to one branch |
| A value everyone is allowed to read, such as today's gold price or the list of cashiers, fetched once a day. | P3 β A public value anyone may fetch |
| The shop issues a signed code worth points; the customer's phone applies it once; head office checks the books at the end. | P4 β A code that carries value (loyalty points, store credit) |
| A borrower's payment slip goes up to the lender; the lender applies it and sends a confirmation back to that borrower. | P5 β A customer pays, the lender confirms |
| Everyone on the team can create and change business partners, and everyone ends up with the same list. | P6 β A team shares the records of a dictionary table |
| My application has its own kind of record (a drawn element) that the dictionary does not know. The team should still share it, and only the kinds I list. | P7 β A team shares records that are not in the dictionary |
| Numbering counters, recent-item lists and session logs belong to one station. Say so, so nobody shares them by mistake. | P8 β A table that stays on its own station |
| I already have a flow for order lines. I want invoice lines to behave the same way. | P9 β Another table, same shape, no code |
4. The patterns¶
P1 β A branch sells, head office answers with stock¶
My branch records sales and sends them to head office at the end of the day. Head office checks stock and sends a replenishment back.
The rows:
[
{"name":"F1 pos-sales-up","treatment":"FACT","role":103,"strategy":100,"dir":"UP","table":"C_OrderLine","carries":["M_Product_ID","QtyEntered","C_UOM_ID"],"group":"POSStation/Date/Order#","schedule":"end-of-day","from":"POS station","to":"HQ"},
{"name":"R1 replenish-rule","treatment":"RULE","role":103,"schedule":"daily","process":"Replenish Report","processId":53267,"emits":"F2 replenishment-down"},
{"name":"F2 replenishment-down","treatment":"FACT","role":103,"strategy":100,"dir":"DOWN","table":"M_MovementLine","carries":["M_Product_ID","M_Locator_ID","M_LocatorTo_ID","QtyEntered","C_UOM_ID"],"group":"Movement doc key","schedule":"on-change","from":"HQ","to":"POS station"}
]
What the compiler says:
F1 pos-sales-up UP C_OrderLine [M] carries 6 of 60 columns
! mandatory columns not carried: C_Order_ID, C_Tax_ID, FreightAmt, LineNetAmt, M_AttributeSetInstance_ID, PriceActual, PriceEntered, PriceLimit, PriceList, Processed, QtyDelivered, QtyInvoiced, QtyLostSales, QtyReserved
i the receiver must already hold: C_UOM, M_Product
R1 replenish-rule RULE (no table)
F2 replenishment-down DOWN M_MovementLine [M] carries 8 of 29 columns
! mandatory columns not carried: M_Movement_ID, Processed
i the receiver must already hold: C_UOM, M_Locator, M_Product
No problems (x). plan 1f7ba5a235d4
Change these when you copy it:
- table and carries: the records that travel and the columns that matter
- group: what makes one record unique, so a resend is recognised
- process: the name of the dictionary process the head-office role runs (and its number), and the role and schedule that run it
Proved by: Β§W-TREAT-STOCKLOOP (scripts/witness_scriptor_treat.js), Β§W-TREAT-LOCAL (scripts/witness_scriptor_treat.js), Β§W-TREAT-TICKET (scripts/witness_scriptor_treat.js).
P2 β A price list pushed to one branch¶
Head office publishes a price list; each branch picks it up when it next connects, once a day.
The rows:
[
{"name":"S1 price-list-seed","treatment":"SEED","role":103,"audience":"addressed","guard":"daily"}
]
What the compiler says:
S1 price-list-seed SEED (no table)
No problems (x). plan f45f3031fb41
Change these when you copy it:
- role: which devices receive it
- guard: daily = ask at most once a day; once = ask only until you have a value
Proved by: Β§W-TREAT-PULLGUARD (scripts/witness_scriptor_treat.js).
P3 β A public value anyone may fetch¶
A value everyone is allowed to read, such as today's gold price or the list of cashiers, fetched once a day.
The rows:
[
{"name":"S2 gold-price","treatment":"SEED","audience":"public","guard":"daily"},
{"name":"S0 roster","treatment":"SEED","audience":"public","guard":"once"}
]
What the compiler says:
S2 gold-price SEED (no table)
S0 roster SEED (no table)
No problems (x). plan aa38969a9a4b
Change these when you copy it:
- audience public: no role is needed to read it
- guard once: for a value that never changes (the roster)
Proved by: Β§W-TREAT-MICROFINANCE (scripts/witness_scriptor_treat.js), Β§W-TREAT-PULLGUARD (scripts/witness_scriptor_treat.js).
P4 β A code that carries value (loyalty points, store credit)¶
The shop issues a signed code worth points; the customer's phone applies it once; head office checks the books at the end.
The rows:
[
{"name":"I1 loyalty-code","treatment":"INSTRUMENT","authorityRole":103,"holderRole":"PHONE","account":"loyalty id"}
]
What the compiler says:
I1 loyalty-code INSTRUMENT (no table)
No problems (x). plan c38313c2d45a
Change these when you copy it:
- authorityRole: who may issue the code
- holderRole: who applies it
- account: what the balance is called
Proved by: Β§W-TREAT-CODE (scripts/witness_scriptor_treat.js).
P5 β A customer pays, the lender confirms¶
A borrower's payment slip goes up to the lender; the lender applies it and sends a confirmation back to that borrower.
The rows:
[
{"name":"P1 borrower-payment-up","treatment":"FACT","role":"BORROWER","strategy":100,"dir":"UP","table":"C_Payment","carries":["C_BPartner_ID","PayAmt"],"group":"Borrower/Date/Slip#","schedule":"at-visit"},
{"name":"R2 payment-rule","treatment":"RULE","on":"P1 borrower-payment-up","process":"ApplyPayment","emits":"N1 payment-confirmation-down"},
{"name":"N1 payment-confirmation-down","treatment":"FACT","role":"BORROWER","dir":"DOWN","group":"Borrower/Date/Confirm#","schedule":"on-change"}
]
What the compiler says:
P1 borrower-payment-up UP C_Payment [M] carries 5 of 105 columns
! mandatory columns not carried: C_Currency_ID, C_DocType_ID, DocumentNo, IsAllocated, IsDelayedCapture, IsOnline, IsPrepayment, IsReceipt, IsReconciled, IsSelfService, Processed
i the receiver must already hold: C_BPartner
R2 payment-rule RULE (no table)
N1 payment-confirmation-down FACT (no table)
No problems (x). plan b0f8e58c044e
Change these when you copy it:
- role: BORROWER here is a label, use your own role
- process: the named process that applies a payment
Proved by: Β§W-TREAT-MICROFINANCE (scripts/witness_scriptor_treat.js).
P6 β A team shares the records of a dictionary table¶
Everyone on the team can create and change business partners, and everyone ends up with the same list.
The rows:
[
{"name":"T1 bpartner-shared","treatment":"FACT","role":103,"strategy":100,"dir":"BOTH","audience":"space","table":"C_BPartner","carries":["Value","Name","C_BP_Group_ID","IsCustomer","IsVendor","IsEmployee","IsOneTime","IsSalesRep","IsSummary","SO_CreditLimit","SO_CreditUsed","SendEMail"],"group":"record","schedule":"on-change"}
]
What the compiler says:
T1 bpartner-shared BOTH C_BPartner [M] carries 15 of 74 columns
i the receiver must already hold: C_BP_Group
No problems (x). plan 1539de1fbf43
Change these when you copy it:
- table and carries: carry every mandatory column or the receiver cannot build the record
- dir BOTH needs audience space; they come as a pair
Proved by: Β§W-MESH-ADD (scripts/witness_scriptor_mesh.js), Β§W-MESH-COLLIDE (scripts/witness_scriptor_mesh.js), Β§W-MESH-FOLD (scripts/witness_scriptor_mesh.js), Β§W-MESH-LATE (scripts/witness_scriptor_mesh.js).
P7 β A team shares records that are not in the dictionary¶
My application has its own kind of record (a drawn element) that the dictionary does not know. The team should still share it, and only the kinds I list.
The rows:
[
{"virtualTable":"bim_element","columns":[{"name":"element_id","cls":"KEY"},{"name":"geom_op","cls":"CODE","mand":true,"values":["GEOM_INSERT","GEOM_EXTRUDE","GEOM_EXTRUDE_POLY","GEOM_SWEEP","GEOM_LOFT","GEOM_REVOLVE"]},{"name":"geom","cls":"OTHER","mand":true},{"name":"pos_x","cls":"MEASURE"},{"name":"pos_y","cls":"MEASURE"},{"name":"pos_z","cls":"MEASURE"},{"name":"ad_client_id","cls":"REF","target":"AD_Client"},{"name":"ad_org_id","cls":"REF","target":"AD_Org"},{"name":"output_guid","cls":"OTHER"},{"name":"input_guids","cls":"OTHER"}]},
{"name":"M1 element","treatment":"FACT","role":"DESIGNER","dir":"BOTH","audience":"space","table":"bim_element","carries":["geom_op","geom","pos_x","pos_y","pos_z","output_guid","input_guids"],"group":"element_id","schedule":"on-change"}
]
What the compiler says:
M1 element BOTH bim_element [virtual] carries 10 of 10 columns
i not checked for system-computed columns (virtual table: the dictionary has no window to judge by)
No problems (x). plan 4f0bfecad90b
Change these when you copy it:
- virtualTable and its columns: describe the record yourself, because there is no dictionary to read
- values: the list of allowed kinds; adding a kind later is adding one value, no code
Proved by: Β§W-DRY-MODELLER (scripts/witness_scriptor_dryrun.js), Β§W-VIRT-COMPILE (scripts/witness_scriptor_virtual.js), Β§W-VIRT-GUARDS (scripts/witness_scriptor_virtual.js).
P8 β A table that stays on its own station¶
Numbering counters, recent-item lists and session logs belong to one station. Say so, so nobody shares them by mistake.
The rows:
[
{"name":"L1 sequence","treatment":"LOCAL","table":"AD_Sequence"},
{"name":"L3 recent-item","treatment":"LOCAL","table":"AD_RecentItem"}
]
What the compiler says:
L1 sequence AD_Sequence [L] carries 11 of 27 columns (PROPOSED by the dictionary, trim it)
L3 recent-item AD_RecentItem [unlisted] carries 3 of 16 columns (PROPOSED by the dictionary, trim it)
No problems (x). plan 90630bd9f19d
Change these when you copy it:
- table: any table that must never leave its station
- a table with no row at all is also local; a LOCAL row just makes the decision visible
Proved by: Β§W-DRY-ERP (scripts/witness_scriptor_dryrun.js), Β§W-ERPPLAN-SCOPE (scripts/witness_scriptor_erp_plan.js).
P9 β Another table, same shape, no code¶
I already have a flow for order lines. I want invoice lines to behave the same way.
The rows:
[
{"name":"X2 invoiceline-up","treatment":"FACT","role":103,"strategy":100,"dir":"UP","table":"C_InvoiceLine","group":"Station/Date/Invoice#","schedule":"end-of-day"}
]
What the compiler says:
X2 invoiceline-up UP C_InvoiceLine [M] carries 45 of 54 columns (PROPOSED by the dictionary, trim it)
i the receiver must already hold: AD_Org, A_Asset, A_Asset_Group, C_1099Box, C_Activity, C_BPartner, C_Campaign, C_Charge, C_CostCenter, C_Department, C_ElementValue, C_OrderLine, C_Project, C_ProjectPhase, C_ProjectTask, C_Tax, C_UOM, M_AttributeSetInstance, M_InOutLine, M_Product, M_RMALine
No problems (x). plan eb37d35036bc
Change these when you copy it:
- copy a row, change name and table; leave carries out to get a PROPOSAL, then trim it
- a proposal is a starting point, never the plan
Proved by: Β§W-FLOW-ZERO-CODE (scripts/witness_scriptor_s1.js), Β§W-TREAT-ZEROCODE (scripts/witness_scriptor_treat.js).
5. Write your own in six steps¶
- Say in one sentence what should move, who sends it and who receives it.
- Find your sentence in the table in section 3 and open that pattern.
- Copy its rows into a new file (a list of rows) and change only what "Change these" says.
- Compile it:
node build/scriptor/compile.js yourfile.json. - Read the report. A line starting
xis a mistake and must be fixed. A line starting!is a question to answer, not always an error. A line startingiis a fact (for example, what the receiver must already hold). - Keep your file: the rows are the source, the plan is generated from them. Never edit a generated plan by hand.
The count after "carries" includes three identity columns (the record id, the client and the organisation) that always travel.
6. Common mistakes β and exactly what you will see¶
M1 β A column name with a typo¶
Start from pattern P1. In the row "F1 pos-sales-up": set carries to ["M_Product_ID","Qty_Entered","C_UOM_ID"].
x unknown column(s): Qty_Entered
Fix: Use the exact dictionary column name (QtyEntered).
M2 β A table that does not exist¶
Start from pattern P1. In the row "F1 pos-sales-up": set table to "C_OrderLine_X".
x no such table: C_OrderLine_X
Fix: Check the table name in the dictionary.
M3 β A required setting left out¶
Start from pattern P1. In the row "F1 pos-sales-up": remove group.
x FACT needs group
Fix: Say what makes one record unique (the group).
M4 β Both ways, but not as a team¶
Start from pattern P6. In the row "T1 bpartner-shared": set audience to "hub".
x FACT dir BOTH needs audience space (a star has no both-ways)
Fix: Both-ways flows are for a team: set audience to space.
M5 β A team flow that only goes one way¶
Start from pattern P6. In the row "T1 bpartner-shared": set dir to "UP".
x FACT audience space needs dir BOTH
Fix: A team shares both ways: set dir to BOTH.
M6 β A kind of movement that is not registered¶
Start from pattern P1. In the row "F1 pos-sales-up": set treatment to "FACTS".
x unknown treatment FACTS
Fix: Use FACT, SEED, RULE, INSTRUMENT or LOCAL, or register a new treatment.
M7 β A made-up kind of column¶
Start from pattern P7. In the declaration of bim_element: change the column pos_x so that cls is MONEY.
x V1 bim_element: pos_x has unknown class MONEY
Fix: Use KEY, REF, MEASURE, DATE, FLAG, CODE, TEXT or OTHER.
M8 β The same column declared twice¶
Start from pattern P7. In the declaration of bim_element: add a second column POS_X.
x V3 bim_element: duplicate column POS_X
Fix: Each column once (names are not case-sensitive).
M9 β Sending a column the system computes¶
Start from pattern P1. In the row "F2 replenishment-down": set carries to ["M_Product_ID","M_Locator_ID","M_LocatorTo_ID","QtyEntered","C_UOM_ID","MovementQty"].
! system-computed columns carried: MovementQty
Fix: Send what a person types; let the receiver compute the rest.
M10 β A mandatory column left out¶
Start from pattern P6. In the row "T1 bpartner-shared": set carries to ["Name","C_BP_Group_ID","IsCustomer","IsVendor","IsEmployee","IsOneTime","IsSalesRep","IsSummary","SO_CreditLimit","SO_CreditUsed","SendEMail"].
! mandatory columns not carried: Value
Fix: Carry it, or be sure the receiver can supply it.
M11 β A rule with nothing to send¶
Start from pattern P1. In the row "R1 replenish-rule": remove emits.
x RULE needs emits
Fix: Name the flow the rule sends.
M12 β A rule that names a flow that does not exist¶
Start from pattern P1. In the row "R1 replenish-rule": set emits to "F9 replenishment-typo".
x RULE emits names a row that does not exist: F9 replenishment-typo
Fix: Use the exact name of the row the rule should send.
7. When the team hears about a save β and who acts on it¶
Background for the people who design flows. Nothing here is a setting; it explains why the rows above can be trusted. Spec: prompts/TEAM_REPLICATION_PLUGIN_STUDY.md Β§9βΒ§10.
A save is part of a transaction. One action of a user can write many records. A Complete on a sales order, for example, writes the shipment, its lines, the invoice and the accounting entries together. They form one transaction: either all of them are kept (commit) or, if anything fails near the end β a closed period, say β all of them are undone (rollback), as if they never existed.
Legacy iDempiere announces a save early. Its "after save" notices (postCreate, postUpdate) are sent the moment each record is saved, while the transaction is still open (PO.java:2799-2804), and they are delivered a little later on another thread. If the transaction is then rolled back, a notice has already gone out about a record that never existed, and a listener reading the database may not find a record it was told about. Legacy also has a separate after-commit hook (Trx.addTrxEventListener β afterCommit, Trx.java:452,761) that careful code can use; the standard notices do not use it.
The twin announces a save once, after the commit. In the SQLite twin one user action becomes one op group, and the kernel commits the whole group in one step. Observing listeners are told after that step succeeded β exactly once, in a fixed order, and never for a group that was rolled back. The screens and messages are the same as legacy; only this behind-the-scenes moment is defined more strictly.
Why replication needs exactly that. Sending a team member's changes to the others is such an observing listener. If it could see a save that was later undone, it would send a ghost document to the whole team β a second source of truth. Told only after the commit, it can only ever send what really happened.
A received record is saved as a replica, and that is a different event. When a device takes in a record from another device, it saves it as a replica (legacy po.saveReplica(true)), and the save raises its own events β after new replication, after change replication, before delete replication β instead of the ordinary after new / after change / before delete (PO.java:2785-2787, 4300). A process chain that must run where the declaration ARRIVES β head office turning a station's sale lines into one completed order with Import Orders (pattern P1) β listens to after new replication. The station's own save of its sale lines raises only the ordinary after new, so the chain never runs at the station: one document, one Complete, at head office only.
What the team layer may and may not do. It carries, orders and admits changes, and it stores what it receives as data. It never completes, posts, numbers or authors a document, computes stock, price or tax, or decides access in its own code; those are the core's, and team rules are expressed as data the core already understands (for example Document Action Access rows).
Status, 2026-10-27: designed and specified (study Β§9βΒ§10); the one event bus that delivers these moments, with the replica save, is being built (study Β§10.5 will record the result). Until it lands, the live team layer observes commits through its own built-in hook, which already runs only after the committed rows are written to the device's store (bim-ootb erp/crud_overlay.js _sidePersist β _teamPush, :1937).
8. Principles behind this design β for anyone building something similar¶
Distilled 2026-10-27 from the team-layer work (prompts/TEAMWORK_FRAMEWORK.md Β§38, prompts/TEAM_REPLICATION_PLUGIN_STUDY.md Β§8βΒ§10). Each one is a rule this project already enforces, not a wish.
- Twin before you innovate. Copy the legacy mechanism exactly; a difference found by running both side by side is fixed on the new side. Users see no change, so adopting it costs them nothing.
- Every server rule becomes signed data. One completer per document, who may use which document action, which keys are trusted, which translation rules apply β each is an owner-signed policy inside the shared log, judged the same way by every device. No referee in the middle.
- Declare where the action happens, derive in one place. A point of sale only declares its lines; one processor completes once and returns what is needed. Mistakes show up as consequences (under-declaring starves your own replenishment), not as online checks.
- Send the minimum. Ids, references, quantities and codes. Every device already holds the master data, so meaning never travels.
- Separate by permission, not by promise. A plugin is given only the hooks its job needs. The team layer may observe saves and store what it receives; it is structurally unable to complete, post or number a document.
- Rows over code. Flows, listeners and access are dictionary rows, never per-window code; progress is counted as X of N over the whole dictionary, never one hand-picked demo.
- A log line is the proof, not a screenshot. Every change starts with a test that fails; every deliberately broken copy must make it fail (a crash does not count); a test that judged nothing says INCONCLUSIVE, never PASS.
- Never silently break users. A change that would invalidate existing teams (for example the shared base fingerprint) needs an upgrade path or an openly stated consequence before it ships.
9. What this guide does not cover yet¶
- All rows here are samples. Nobody has authored real business flows yet; the business owner decides what really moves.
- Only the Modeller and the ERP team scope read their plan in the live applications today. The point-of-sale and micro-finance patterns run in the simulator.
- The plans that ship are not signed by default. Signing works (
build/scriptor/sign_plan.js) and a team pins the keys it trusts when it is founded; every member of a team can still read everything in that team (audience keys are not built). - The compiler checks names and shapes. It cannot know whether a row is a good business decision.