Skip to content

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

  1. Say in one sentence what should move, who sends it and who receives it.
  2. Find your sentence in the table in section 3 and open that pattern.
  3. Copy its rows into a new file (a list of rows) and change only what "Change these" says.
  4. Compile it: node build/scriptor/compile.js yourfile.json.
  5. Read the report. A line starting x is a mistake and must be fixed. A line starting ! is a question to answer, not always an error. A line starting i is a fact (for example, what the receiver must already hold).
  6. 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.

  1. 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.
  2. 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.
  3. 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.
  4. Send the minimum. Ids, references, quantities and codes. Every device already holds the master data, so meaning never travels.
  5. 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.
  6. 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.
  7. 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.
  8. 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.