01Finance · Business workflow

Build an expense workflow with a clear decision history.

Connect each claim to its cost centre, review decision and payment record. Give employees a way to correct a mistake while keeping the original submission visible.

CanLang builds the app
Records, permissions, workflows and connected screens.
Soda OS runs the factory
AI development, persistent Projects and reviewed changes.

Illustrative application and company rules. CanLang also works independently of Soda OS.

View CanLangBack to app
Expenses.canExcerpt

Approval and payment records

When
 crud CostCentre by=finance fields=code,name,active delete=none
 crud Expense by=members fields=purpose,amount,cost_centre update=none delete=none
 # A reviewer approves someone else's positive expense claim.
 scenario approve(expense:Expense,note:text?) by=reviewer
  require expense.status==submitted and expense.submitted_by!=actor
  require expense.cost_centre.active
  do set expense {status=approved,decision_note=note,approved_by=actor,approved_at=now}
 # Finance records a payment only after the claim is approved.
 scenario record_payment(expense:Expense,reference:text) by=finance
  require expense.status==approved and trim(reference)!=""
  do set expense {status=paid,payment_reference=trim(reference),paid_at=now}

Expenses · App preview

Expenses
EXP-0085 · Sam

Client travel

€85
Cost centre
CLIENT
Review
Alex · Approved
External payment ref.TRAVEL-001
Paid

Payment recorded only; no money transferred.

Illustrative application interface · sample records.

02CanLang · Application model

People, records and rules.

Your department decides how the work should run. This example expresses those decisions in CanLang: the records it stores, who can access them, and the rules each action must follow.

Records the CanLang app keeps

Illustrative first-version records and relationships
Claim · Client travel€85
Submitter
Sam
Cost centre
CLIENT · Client services
Review
Alex · separate reviewer
After approval, Finance recordsExternal payment recordTRAVEL-001

The reference records a payment made through the company’s payment process. It does not transfer money.

Claims connect the purpose, positive amount, submitter and cost centre to a recorded review decision. A payment record has a nonempty reference to an external payment.

First-version workflow

  1. Submitted
  2. Approved
  3. Paid

People and app access

Employee
Submit a purpose, positive amount and cost centre; read their own claims.
Reviewer
Read claims and approve another person's Submitted claim against an active cost centre.
Finance
Maintain cost centres, read the queue and record a payment reference after approval.

What these permissions mean

Expected behavior · illustrative cases, not executed test results

Allowed
Alex approves Sam’s eligible Submitted claim.
Refused
Sam approves Sam’s own claim.
CanLang · Explicit business rules

What the app protects

The department sets these requirements. The CanLang application expresses the permissions and workflow restrictions.

  1. Self-approval and repeated approval are refused.

  2. Submitted purpose, amount, cost centre and submitter stay protected.

  3. Only Finance records payment, after approval with a nonempty reference. Paid records remain protected.

  4. Paid records an external payment; this action does not transfer money. Reviewer and Finance permissions may be held by the same person in this example.

These are application permissions and business rules. Soda OS and Forgejo govern the separate development and code-review process in section 04.

See how these records and responsibilities connect
Illustrative workflow · Finance
Client travel€85Submitted by Sam
Alex's approval
Finance payment record
Original details protected
Submitted → Approved → Paid

Claims retain their purpose, amount, cost centre and submitter. A separate reviewer decides.

Paid records an external payment; it does not transfer money.

Read this app’s CanLang source
03CanLang · Application experience

Use the app. Check the result.

Follow the first-version workflow through its screens, then check that the app allows the right actions and protects the right records.

A walkthrough of the first version

  1. Sam submits client travel

    Sam submits the claim against an active cost centre and follows their own record.

  2. Alex reviews the claim

    Alex's approval is a business decision, distinct from reviewing the code that implements it.

  3. Finance records payment

    Finance records the payment reference after approval. This does not initiate a money transfer.

View CanLangBack to app
Expenses.canExcerpt

Approval and payment records

When
 crud CostCentre by=finance fields=code,name,active delete=none
 crud Expense by=members fields=purpose,amount,cost_centre update=none delete=none
 # A reviewer approves someone else's positive expense claim.
 scenario approve(expense:Expense,note:text?) by=reviewer
  require expense.status==submitted and expense.submitted_by!=actor
  require expense.cost_centre.active
  do set expense {status=approved,decision_note=note,approved_by=actor,approved_at=now}
 # Finance records a payment only after the claim is approved.
 scenario record_payment(expense:Expense,reference:text) by=finance
  require expense.status==approved and trim(reference)!=""
  do set expense {status=paid,payment_reference=trim(reference),paid_at=now}

Illustrative first-version application

Expenses
WorkspaceClaims Internal app
Expense register

Expense claims

October · EUR
EXP-0085 · expense claim

Client travel

€85EUR

Submitted by Sam · 14 Oct, 09:05

Approved by Alex14 Oct, 11:20
External payment record
TRAVEL-001Paid

Recorded by Finance · 15 Oct, 10:00

Reference to the company’s payment process. No money is transferred by this app.

Submitted purpose, amount, cost centre and submitter are read-only.

View claim history
  1. Sam submitted €8514 Oct, 09:05 · CLIENT cost centre active
  2. Alex approved the claim14 Oct, 11:20 · independent reviewer
  3. Finance recorded TRAVEL-00115 Oct, 10:00 · external payment reference · paid record protected

Illustrative application interface. The linked CanLang browser demo has the smaller scope described below.

Your department · Acceptance checklist

Does this version do the job?

Use these expected outcomes to accept the CanLang app. Try the relevant roles, including actions that must be refused.

Checks to perform on the illustrative first version; these are not reported test results.

Expense approvals · First version

Expected outcomes

  1. Positive submitted claims use an active cost centre and retain their original details.

  2. A separate authorised reviewer can approve; self-approval and repeated approval fail.

  3. Payment recording requires Finance authority, prior approval and a reference.

  4. Paid records and existing access remain protected.

Reference: the sample records behind these checks

Sam submits client travel

Claim
Client travel
Amount
€85
Status
Submitted

Alex reviews the claim

Submitter
Sam
Reviewer
Alex
Status
Approved

Finance records payment

Payment
External payment recorded
Reference
TRAVEL-001 · illustrative
Status
Paid

Try a focused CanLang demo

The compact demo covers submission and approval. The larger source adds cost centres and recorded external payments. The return-and-replacement continuation is a separate version; it is not part of the browser demo.

Try the expense example in CanLang
04Soda OS · Software factory

Develop and review the app.

Your department defines the result. Soda OS coordinates authorised coding agents in a persistent Project; Forgejo records the work and applies the review and merge policy.

Start from the business request

“We need employees to submit expenses against our cost centres, a separate reviewer to approve eligible claims, and Finance to record payments without rewriting what was submitted.”

Follow the development process

Move through the stages below: accepted direction, dependent work items, CanLang source, independent review and policy-governed integration.

Corrections and human approval apply where required by repository policy. Merging code and deploying the application are separate actions.

The request, work items and review cases are illustrative, not a recording of a completed factory run.

1 / 6 · Finance owns the direction

Agree on a clear expense approval process.

Finance opens New Project and answers “What do you want to build?” Explore ideas develops the request: Sam submits Client travel for EUR 85.00; a separate reviewer approves it. Finance accepts or changes the experience in “Review your product direction”.

What carries forward

An accepted expense workflow. Material questions return to Finance instead of becoming hidden assumptions.

Example accepted direction

Example accepted direction

Expense claims, with separate approval

  1. Capture the claim accurately

    Store purpose, exact money amount, submitter and status. Require a positive amount; employees read their own claims and reviewers read the review queue.

  2. Keep approval independent

    Only a reviewer approves a submitted claim. Block self-approval even when the submitter also has the reviewer role.

  3. Preserve the approved claim

    Block repeated approval and lock purpose, amount and submitter after approval. Keep claim updates and deletion unavailable.

Illustrative workflow artifact from the selected department journey.

2 / 6 · Soda coordinates the work

Give each approval requirement a prerequisite.

Authorize a finite planning allowance and choose a repository. Soda creates native Forgejo work items with acceptance criteria and explicit dependencies. Execution needs the repository’s authorization, accounts and capacity.

What carries forward

Accepted issue criteria and prerequisite outcomes. Unresolved dependencies or unanswered requirements interrupt the work.

Example native work items

Example native work items

A dependency plan for Sam’s claim

  1. Define the Expense record and access

    Store exact money and the submitting actor. Require positive amounts, employee access to own claims and reviewer access to the queue.

  2. Implement submission and approval

    Depends on the record and access rules. Accept submissions; require reviewer role, submitted status and a different submitter before approval.

  3. Connect the claim form and review list

    Depends on records and operations. Check EUR 85.00 stays exact, another reviewer can approve, and self-approval and repeated approval are refused.

Illustrative workflow artifact from the selected department journey.

3 / 6 · The coding agent implements

Keep the expense policy with the claim.

Soda assesses readiness and prepares the repository’s persistent Project and assigned coding checkout. The selected coding agent receives the accepted issue, repository context and authorized account access, then reports the candidate and evidence. Soda OS publishes the pull request.

What carries forward

A candidate pull request at an exact revision. Spaces shows live agent activity and intervention needs alongside Forgejo.

Expenses.can · source excerpt

Positive amounts and independent approval

Given
 role reviewer
 Expense { purpose:text trim, amount:money, submitted_by:user server=actor, status:enum(submitted,approved)=submitted }
 policy Expense read=members where=row.submitted_by==actor
 policy Expense read=reviewer
 invariant Expense: row.amount.minor>0
 lock Expense fields=purpose,amount,submitted_by when=row.status==approved
When
 crud Expense by=members fields=purpose,amount update=none delete=none
 # Approve a submitted expense from another teammate.
 scenario approve(expense:Expense) by=reviewer
  require expense.status==submitted and expense.submitted_by!=actor
  do set expense {status=approved}
4 / 6 · Another agent checks the candidate

Catch self-approval, then review the correction.

A separate agent session reviews the exact candidate alongside required checks. Findings return to the coding role; revised candidates receive fresh checks, with further independent review when evidence or repository policy requires it.

What carries forward

Findings and evidence belong to the revised candidate. These cases illustrate review expectations rather than report a completed run.

Illustrative review and correction

Illustrative review and correction

A reviewer role alone is insufficient

  1. Finding: Sam could approve Sam’s claim

    If a candidate checked only the reviewer role, Sam could approve their own EUR 85.00 expense. That violates Finance’s accepted requirement.

  2. Correction: require a different submitter

    The coding role checks submitted status and submitter identity, alongside reviewer authority and the positive-amount invariant shown above.

  3. Fresh review: exercise the accepted cases

    Alex as reviewer can approve Sam’s submitted claim; Sam as reviewer cannot. Refuse ordinary employee approval, repeat approval, zero and negative amounts. Preserve EUR 85.00 exactly and lock approved fields.

Illustrative workflow artifact from the selected department journey.

5 / 6 · Forgejo policy governs integration

Integrate when the current policy permits it.

Soda merges automatically when required review and checks pass for the exact candidate and repository policy permits it. When policy requires human approval, that approval is part of the path. Changed or missing evidence prevents integration.

What carries forward

Confirmed integration lets Soda reassess dependent work. Merge and application deployment remain separate actions.

Integration conditions

Integration conditions

The candidate must satisfy all three

  1. The revision still matches

    The pull request’s current candidate is the revision that was checked and reviewed.

  2. Required evidence is current

    Required checks, review and any required human approval satisfy the repository’s rules.

  3. Native merge is confirmed

    Forgejo records integration and the corresponding issue outcome. A published pull request alone does not establish integration.

Illustrative workflow artifact from the selected department journey.

6 / 6 · Finance and employees use the workflow

Connect expense submission and independent review.

The same CanLang source describes the claim, access rules, approval action and screen. Try the matching browser example: submit Client travel as Sam, then switch to Alex as reviewer to approve EUR 85.00.

What carries forward

A tool shaped around Finance’s work. The next accepted improvement enters the same factory process.

Try the expense example
View CanLangBack to app
Expenses.canExcerpt

Approval and payment records

When
 crud CostCentre by=finance fields=code,name,active delete=none
 crud Expense by=members fields=purpose,amount,cost_centre update=none delete=none
 # A reviewer approves someone else's positive expense claim.
 scenario approve(expense:Expense,note:text?) by=reviewer
  require expense.status==submitted and expense.submitted_by!=actor
  require expense.cost_centre.active
  do set expense {status=approved,decision_note=note,approved_by=actor,approved_at=now}
 # Finance records a payment only after the claim is approved.
 scenario record_payment(expense:Expense,reference:text) by=finance
  require expense.status==approved and trim(reference)!=""
  do set expense {status=paid,payment_reference=trim(reference),paid_at=now}

Expenses · App preview

Expenses
EXP-0085 · Sam

Client travel

€85
Cost centre
CLIENT
Review
Alex · Approved
External payment ref.TRAVEL-001
Paid

Payment recorded only; no money transferred.

Illustrative application interface · sample records.

Read the application outcomes

Client travel → EUR 85.00 → Approved

  • Sam sees Sam’s claims

    Submit a positive exact money amount and follow its status in the personal list.

  • Alex reviews the queue

    Approve another employee’s submitted claim. Reviewer authority never permits self-approval.

  • Approval preserves the details

    Repeated approval is refused and approved fields stay locked. The browser example mirrors the rules with local page state and a reset control.

Read the factory handbook
Where this walkthrough comes from

The factory sequence and interface names follow the product documents and source code. Application behavior follows the matching complete examples on the CanLang page.

05CanLang app · Continued in the Soda OS factory

Correct a claim without losing its history.

The business changes its requirements. Revise the CanLang app in the same persistent Project, use the factory to develop and review the change, and check what must keep working.

What changes in the application

Illustrative correction · €85 claim
Original submissionReturned · Locked

€85 travel claim

  • Sam · Original cost centre
  • Reason, actor and time retained

Only Sam creates one direct replacement

One linked replacementSubmitted · Fresh review

€85 travel claim

  • Sam · Corrected cost centre
  • No approval or payment carried forward

Only the submitter creates the replacement. Another return continues the chain without parallel branches.

Initial rule
A submitted claim receives independent review, then Finance records external payment.
New rule
A reviewer returns someone else's Submitted claim with a reason, actor and time. Only its submitter can create one direct linked replacement, starting at Submitted with fresh validation and review.
What stays protected
The returned original remains immutable and cannot progress to approval or payment. Approvals and payments never copy forward. Approved and paid claims are outside the correction path; existing ownership and visibility apply to the chain.

The revised app experience

View CanLangBack to app
ExpensesCorrection.canRule-change excerpt

Return and replace a claim with fresh review

When
 crud CostCentre by=finance fields=code,name,active delete=none
 crud Expense by=members fields=purpose,amount,cost_centre update=none delete=none
 # Return only an unapproved submission, keeping its details intact.
 scenario return_for_correction(expense:Expense,reason:text) by=reviewer
  require expense.status==submitted and expense.submitted_by!=actor
  require trim(reason)!=""
  do set expense {status=returned,returned_by=actor,returned_at=now,return_reason=trim(reason)}
 # A unique immutable key admits one replacement per original, atomically.
 # Only this guarded scenario supplies the key; ordinary creation cannot set it.
 scenario replace_submission(original:Expense,purpose:text,amount:money,cost_centre:CostCentre) by=members
  require original.status==returned and original.submitted_by==actor
  require amount.minor>0 and cost_centre.active and trim(purpose)!=""
  do create Expense {purpose,amount,cost_centre,replaces=original,replaces_key=original.id} as replacement
 # A reviewer approves someone else's positive expense claim.
 scenario approve(expense:Expense,note:text?) by=reviewer
  require expense.status==submitted and expense.submitted_by!=actor
  require expense.cost_centre.active
  do set expense {status=approved,decision_note=note,approved_by=actor,approved_at=now}
 # Finance records a payment only after the claim is approved.
 scenario record_payment(expense:Expense,reference:text) by=finance
  require expense.status==approved and trim(reference)!=""
  do set expense {status=paid,payment_reference=trim(reference),paid_at=now}
Illustrative CanLang rule-change excerpt · open the complete revised source for records, permissions and interfaces.Read the complete source

Expenses · After a rule change

Expenses
WorkspaceClaims Internal app
Expense register

Claims to review

October · EUR
EXP-0092 · replacement claim

Client travel

€85EUR

Submitted by Sam · 15 Oct, 09:15

SubmittedFresh review required

No approval or payment copied from the original. Alex may review again; Sam cannot approve.

Approval
Not recorded
Payment
Not recorded

Submitted purpose, amount, cost centre and submitter are read-only.

Inspect returned original · EXP-0091
Client travel · €85Returned
Original cost centre
OPS · Operations
Returned by
Alex · 14 Oct, 11:20
Reason
Wrong cost centre. Please use Client services for this travel.
Replacement
EXP-0092 · created by Sam · 15 Oct, 09:15

Original retained unchanged. It cannot be approved or paid; its one direct replacement starts a fresh review.

Correction history
  1. EXP-0091 · Returned by Alex14 Oct, 11:20 · wrong cost centre · original locked
  2. EXP-0092 · Submitted by Sam15 Oct, 09:15 · corrected active cost centre · no decision yet

A further return continues from EXP-0092. Each returned claim permits one direct replacement. Existing approved and paid claims retain their history.

Illustrative application interface · sample records.

Read the full rule, protections and review checks
Illustrative correction chain

Correct the claim. Retain the original.

Original submissionReturned for correction
€85Client travel · Sam
Cost centre
Original selection retained
Return
Alex · reason + time recorded

Reason: the claim uses the wrong cost centre.

Immutable · cannot be approved or paid
Current replacementSubmitted · fresh review required
€85Client travel · Sam
Cost centre
Corrected active centre
Approval / payment
Nothing copied forward
  1. Submitted
  2. Independent approval
  3. Finance records external payment
Alex may review again; Sam cannot approve.
Returned again?Continue from that replacement

Each returned submission has at most one direct replacement. Keep the visible chain; never create parallel replacement branches.

A return requires a reason, actor and time. Sam may leave it unresolved. Existing Submitted claims can use the return path; approved and paid claims keep their records. The chain retains existing ownership and visibility.

  • Sam chose the wrong cost centre for the €85 travel claim. Alex requests correction; Sam may submit a linked replacement with the corrected centre or leave the return unresolved.
  • At most one direct replacement is allowed per returned submission. Another return continues the visible chain from that replacement, without parallel branches.
  • Each replacement satisfies positive amount, active cost-centre and independent business-approval rules. Alex may review again; the reviewer must differ from Sam.
  • Only the current replacement progresses toward approval and payment. Returned records cannot be approved or marked paid.
  • Existing Submitted claims may enter the return path. Existing approved and paid records keep their history.
  • An illustrative code-review finding is an old approval copied onto a correction; the change must require fresh review instead. This is not general duplicate-expense detection.
Read the revised CanLang source and version coverage
06Your company · Requirements and next steps

Make the rules your own.

Use this example to agree a first useful workflow with its owner and technical counterpart. Identity, purchasing, inventory and other connections need their own evaluation.

Cost centres
Which centres exist, and who maintains them?
Reviewer authority
Who may review each claim, and under which limits?
Correction policy
Which reasons and replacement history must be retained?
External payment
Who records the payment and its reference?
Connections
Evaluate receipts and accounting connections separately.

Explore another workflow · Read the factory handbook · Get help