A multi-model Dirigible intent sample: a Billing domain split into six independent projects
that reference each other across models. Each subfolder is its own Dirigible project with a
single *.intent file at its root (the source of truth) - open it in the Intent Editor and click
Generate to produce the model files and then the running app.
It demonstrates three capabilities of the intent layer:
- Cross-model references (
uses:+ a relationmodel:) - an entity reuses master data owned by another project instead of redefining it. The owner owns the single table; consumers store an integer FK and render a dropdown sourced from the owner's REST service (no duplicated table). - Many-to-many via an explicit intermediate entity -
SalesInvoiceCustomerPaymentlinks an invoice to a customer payment with a partialamount, where the two sides live in different projects. - Faithful field attributes -
unique,precision/scale,calculatedOnCreate, and entityaudit: true. - Depends-On (
dependsOn: { relation, valueFrom?, filterBy? }) - cascading, narrowed and auto-populated form controls driven by a sibling relation, covering all six canonical cases of codbex-sample-model-depends-on - see the table below.
| Project | Owns | References (cross-model) |
|---|---|---|
uoms |
Dimension, UoM | - |
countries |
Country | - |
currencies |
Currency, CurrencyRate | - |
customers |
Customer | Country, Currency |
customer-payments |
CustomerPayment | Customer, Currency |
products |
Product | UoM |
sales-invoices |
SalesInvoice, SalesInvoiceItem, SalesInvoiceCustomerPayment, settings | Customer, Currency, UoM, CustomerPayment, Product |
SalesInvoice -> SalesInvoiceItem is a 1:n composition (local). SalesInvoice <-> CustomerPayment is
the cross-model n:m, modelled by the SalesInvoiceCustomerPayment intermediate entity.
The navigation project is not an intent project: it defines the shared-shell
navigation groups once (getPerspectiveGroup() for master-data, sales, payments, settings),
which the domain entities reference via group:.
The Depends-On feature makes a form control react to a sibling to-one relation: filter a dropdown's options, or auto-populate a value, from the trigger's selected record. This sample maps every case of the reference codbex-sample-model-depends-on:
| # | Where | Dependent | Trigger | Behaviour |
|---|---|---|---|---|
| 1 | Customer form | City |
Country |
cascade: only the chosen country's cities (filterBy: Country) |
| 2 | Invoice item dialog | UoM |
Product |
narrow-to-referenced: auto-selects the product's base unit (valueFrom: UoM) |
| 3 | Invoice item dialog | price |
Product |
auto-populate: the product's default price (valueFrom: price) |
| 4 | Allocation form | Customer |
SalesInvoice |
related-entity navigation: narrowed to the invoice's customer (valueFrom: Customer) |
| 5 | Allocation form | CustomerPayment |
Customer |
cascade: only that customer's payments (filterBy: Customer) |
| 6 | Allocation form | amount |
CustomerPayment |
auto-populate: defaulted to the payment's amount, editable (valueFrom: amount) |
Cases 1, 2, 4 and 5 cross model boundaries (trigger and/or target owned by another project).
valueFrom defaults to the trigger target's primary key, filterBy to the dependent's own target
primary key - so a plain cascade only declares filterBy and a narrow-to-referenced only
valueFrom. The allocation Customer FK is nullable and cosmetic: rows created by the
auto-allocation settlement leave it empty; it exists to guide manual allocation (pick the
customer -> see only their payments -> amount pre-filled).
Five management reports over the invoice data (sales-invoices.intent
reports:), rendered by the generated Harmonia report pages (sidebar Reports + dashboard tiles):
| Report | Shows |
|---|---|
InvoicesByCustomer |
invoice count, revenue, paid and outstanding balance per customer |
InvoicesByStatus |
pipeline overview - count and value per status |
OverdueInvoices |
unpaid invoices past their due date (listing with a compound filter) |
SalesByProduct |
quantity sold and revenue per product |
MonthlyRevenue |
income, net and VAT aggregated per month (month(date) bucket) |
Customer and Product are cross-model dimensions: the report joins the owning model's table
and shows the name instead of the raw FK id. Every report table offers typed per-column filters
(date ranges, number ranges, text contains) applied server-side - pagination, count and CSV export
all reflect them.
Each domain project generates its own standalone app shell (handy for running or testing a single
domain), and contributes its entities as grouped perspectives to the platform's shared shell.
Open the shared shell at /services/web/application/ after publishing all projects and you get
one app with a single grouped sidebar:
- Master Data - UoM, Country, Currency, Customer
- Sales - Sales Invoices
- Payments - Customer Payments
- Settings - the nomenclatures (Number, statuses, methods)
Each entry opens that domain's screen embedded in the one shell, so the user never jumps between
per-project UIs. (Grouping is driven by group: on each entity + the navigation project; it
requires the intent shared-shell support, PRs eclipse-dirigible/dirigible#6089 and #6090.)
-
Start Dirigible and open the IDE at
http://localhost:8080(default credentialsadmin/admin). -
Clone this repo into your workspace. In the IDE's Git perspective use
https://github.com/dirigiblelabs/sample-intent-multi-model.git; each top-level subfolder (uoms,countries,currencies,customers,products,customer-payments,sales-invoices,navigation) becomes a project. -
Generate the model files, leaf-first so cross-model references resolve (each consumer reads the owner's already-generated
.model). For each project, double-click its*.intent, then click Generate in the Intent Editor, in this order:uoms,countries,currenciescustomers,productscustomer-paymentssales-invoices
(The
navigationproject has no intent — it just declares the sidebar groups and is published as-is.) Generate writes the.edm/.model/.bpmn/.form/.report/.roles/.csvimnext to each intent, and chains the model-to-code generation (Java DAO/REST + Alpine.js Harmonia UI — the default recipe). -
Publish everything (Workbench → Publish All). Cross-model dropdowns call the owner project's REST service (
/services/java/<ownerProject>/...), so every owner must be live; the.csvimseeds load the nomenclatures (payment methods, sent methods, invoice statuses, …). -
Open the shared shell:
http://localhost:8080/services/web/application/. You land on a dashboard with one KPI tile per entity and a single grouped sidebar (Master Data / Sales / Payments / Settings) — every app embedded in this one shell.
This is the end-to-end flow the sample is built to show. The SalesInvoiceApproval process
(trigger: { onCreate: SalesInvoice }) walks an invoice through Approve → Issue → Send, with a
Reject branch that cancels it.
- Create a customer — either up front via Master Data → Customer → New (fill name, country,
currency, save), or inline while creating the invoice: the invoice's Customer dropdown has
a New action that opens the Customer create form in a dialog and selects the new record on
save (this is a cross-model link — the customer lives in the
customersproject). - New invoice — the create (header) form. Go to Sales → Sales Invoices → New. You get the
document header:
Number(auto-filled by acalculatedOnCreateexpression, read-only),Date(required),Due,Customer,Currency, payment/sent method. The totals (Net/Vat/Total) are still 0 — there are no line items yet. - Create. Saving the header does two things: the page switches to edit mode and the
line-items table appears (you can only add items once the header exists), and — because the
process triggers
onCreate— theSalesInvoiceApprovalprocess starts and an Approve task is created for this invoice (DRAFT, status 1). - Add an item. In the items table click Add and enter name, quantity, price, discount, UoM. The line's Net = Quantity × Price, Vat = round(Net × 0.2, 2) and Total = Net + Vat − Discount are calculated live; saving the line recomputes the invoice's header totals (Net/Vat/Total) synchronously. Add as many lines as you need.
- Save the document.
- Approve (or Reject). Open the invoice's Approve Sales Invoice task (from the record's
inline tasks or the Process Inbox). It's a read-only card showing the invoice's current
values —
Number,Date,Customer(by name), andTotalnow reflecting the items you added (not the 0 it had at creation), plusStatus. Three buttons: Approve (green), Reject (red), Close (just dismisses the form, leaving the task open).- Approve → the decision continues to Issue; the invoice becomes APPROVED.
- Reject → the invoice is CANCELLED and the process ends.
- Issue. After approval, the Issue Sales Invoice task appears (read-only, Issue button in blue). Completing it marks the invoice ISSUED and continues to Send. (In a full codbex setup the definitive invoice number is generated at this step.)
- Send. The Send Sales Invoice task shows the
Sent Method; the Send button marks the invoice SENT and the process ends.
Throughout, each task form fetches the live invoice when you open it (the process context holds only the invoice id), so it always shows current data — the total you see on the Approve form already includes items added after the task was created.
The default code-gen template is Java + Harmonia. Each project also generates its own standalone
SPA at /services/web/<project>/gen/<genFolder>/index.html, useful for testing one domain in
isolation; the shared shell at /services/web/application/ simply aggregates them.
- Project folder names equal each intent's
name:(and so the table prefix and gen folder), which is why theuses:entries need no explicitproject:- it defaults to the model alias. SalesInvoice.numberuses acalculatedOnCreateexpression. This stand-alone sample usesjava.util.UUID.randomUUID().toString()so it compiles without an external service; in codbex the number is produced byNumberGeneratorService- swap the expression when that service is present.