When is an invoice
ready to send?
This example covers one workflow: an owner sending an invoice.
1. Agree on the business rules
- Only the owner can send an invoice.
- The invoice must be a draft.
- The invoice needs at least one line item.
- Sending changes its status to sent.
- An empty or already sent invoice is refused with a specific reason.
An engineer chooses the database, API routes, and framework when designing the implementation.
2. Read the complete model
domain invoicing
"The business rules for sending an invoice."
mode model-first
dimension actor = owner | client
concept DenyCode
field copy: string
values
NOT_DRAFT "Only a draft invoice can be sent."
EMPTY_INVOICE "An invoice needs at least one line item."
concept Invoice
dimension status = draft | sent
concept LineItem
field invoice: Invoice
shape Draft of Invoice
where status = draft
shape Sent of Invoice
where status = sent
shape Empty of Invoice
where no LineItem
action SendInvoice of Invoice
"The owner sends an invoice."
by owner
requires Draft else NOT_DRAFT
requires not Empty else EMPTY_INVOICE
effect
status = sent
scenario SendingADraft
with Draft, LineItem
the owner sends it via SendInvoice -> Sent
scenario SendingAnEmptyDraft
with Draft, Empty
the owner sends it via SendInvoice -> denied EMPTY_INVOICE
scenario SendingTwice
with Sent, LineItem
the owner sends it again via SendInvoice -> denied NOT_DRAFT
The .meant download contains exactly the model shown above.
What the declarations mean
Draft, Sent, and Empty describe conditions. These conditions can overlap: a draft can also be empty. LineItem.invoice connects line items to their invoice.
SendInvoice restricts the actor, checks the two preconditions, and declares the change to status. The refusal codes are part of the business vocabulary, so they are declared too.
3. Inspect the recorded evidence
These scenario replays were recorded from the CLI. For each one, the simulator creates an example that satisfies the starting conditions, then executes the modeled action.
| Scenario | Recorded outcome | Expectation |
|---|---|---|
SendingADraft | Lands in Sent | Matched |
SendingAnEmptyDraft | Denied EMPTY_INVOICE | Matched |
SendingTwice | Denied NOT_DRAFT | Matched |
Inspect the full recorded JSON output ↗
Each replay matches its expected outcome. This checks that modeled execution; it does not prove the application implements the rule or cover every possible behavior.
Warnings in this example
The structural check reports zero errors, zero warnings, and two informational notices. Simulation adds two warnings because this focused slice does not model actions that create draft or empty invoices. It also reports Sent as unprobeable for an exit: no supported outgoing transition is declared.
A fuller lifecycle model would also describe how invoices are created, edited, and handled after sending.
4. Make the engineering choices
An engineer can implement these rules in a monolith, a service with a background worker, or an existing system. Separately decide storage, interfaces, transaction boundaries, retries, and delivery guarantees.
The example’s status = sent effect only models a status change. It does not prove that an email was delivered. If delivery is a business requirement, model the observable outcome and its failure cases explicitly.
Translate expectations into application tests
- An owner can send a draft with a line item.
- A client cannot perform the owner-only action.
- An empty draft is rejected with the expected refusal.
- An already sent invoice is rejected.
- A successful send persists the expected status change.
Run these tests against the application. The Meant CLI does not execute them.
Next: work with an agent ↗