Create /expense_tracker
Create a process named /expense_tracker.
Before creating it, ask the admin which currency the tracker should use, for example PLN, EUR, or USD. The selected currency applies to the whole process.
Create a responsive custom Start form so users do not need to enter JSON manually.
Variables
Create one start variable:
$input— object containing the action and its arguments.
Create internal variables:
$action— string used by Branch.$notification_users— string containing a user expression.$notification_text— string.$add_result— object.$remove_result— object.$summary_result— object.$entries_result— object.
Actions
Support exactly:
add_expense
remove_expense
show_summary
show_entries
add_expense
{
"action": "add_expense",
"bill_with": "b4rtaz",
"amount": 120.5,
"split": "50:50",
"description": "Dinner"
}
bill_with— required AilaFlow user.amount— positive monetary amount.split—"50:50"or"0:100".description— optional.
The process starter always pays the complete expense.
50:50 means the other user repays half.
0:100 means the other user repays the full amount.
remove_expense
{
"action": "remove_expense",
"expense_id": "expense-id"
}
Anyone may remove an active expense.
show_summary
{
"action": "show_summary"
}
Returns the current balances of the process starter.
show_entries
{
"action": "show_entries",
"user": "b4rtaz",
"page": 1
}
Returns expenses between the process starter and the specified user.
user— required.page— optional positive integer, default1.- return 50 records per page;
- return whether another page exists.
User handling
Get the process starter with:
const startedBy = await ailaflow.getStartedBy();
Normalize every username supplied through $input:
- trim whitespace;
- lowercase it;
- add
@if missing.
For example:
B4RTAZ
@B4RTAZ
b4rtaz
all become:
@b4rtaz
AilaFlow usernames are lowercase.
Validate supplied users with:
await ailaflow.userExists(user);
Fail if the user does not exist.
Do not allow an expense with yourself or history lookup with yourself.
User expressions
User-expression operators must be lowercase.
Use or when addressing multiple users:
@alice or @bob
Never generate:
@alice AND @bob
For property expressions, follow AilaFlow syntax such as:
@{.team = "finance" and .access_level = "c3"}
Use only lowercase and and or.
Money and splitting
Store money as integer minor units.
For two-decimal currencies:
const amountMinor = Math.round(input.amount * 100);
Do accounting calculations using integers.
For 50:50:
const owedMinor = Math.floor(amountMinor / 2);
If the amount is odd in minor units, the payer absorbs the extra unit:
101 minor units
payer: 51
other user: 50
For 0:100:
const owedMinor = amountMinor;
Tables
Use:
#expenses
#expense_movements
Tables are dynamic; do not create them separately.
#expenses
Store one row per expense.
Columns:
_id
pair_key
payer
bill_with
amount_minor
owed_minor
currency
split
description
status
created_at
created_by
removed_at
removed_by
Example:
{
"_id": "expense-id",
"pair_key": "@alice:@bob",
"payer": "@alice",
"bill_with": "@bob",
"amount_minor": 12050,
"owed_minor": 6025,
"currency": "PLN",
"split": "50:50",
"description": "Dinner",
"status": "active",
"created_at": "2026-09-15T20:00:00.000Z",
"created_by": "@alice"
}
Omit unset removal fields.
Never physically delete expenses.
Canonical pair key
Use the same key regardless of who paid:
function createPairKey(user1, user2) {
return [user1, user2].sort().join(':');
}
Alice/Bob and Bob/Alice therefore both produce:
@alice:@bob
#expense_movements
Create two signed movement rows for every expense.
Columns:
_id
expense_id
user
with_user
amount_minor
currency
status
Positive means:
with_user owes user
Negative means:
user owes with_user
Example:
{
"_id": "expense-id:@alice",
"expense_id": "expense-id",
"user": "@alice",
"with_user": "@bob",
"amount_minor": 6025,
"currency": "PLN",
"status": "active"
}
and:
{
"_id": "expense-id:@bob",
"expense_id": "expense-id",
"user": "@bob",
"with_user": "@alice",
"amount_minor": -6025,
"currency": "PLN",
"status": "active"
}
Use deterministic IDs:
expense_id + ":" + user
Main Script
Use one Script step for business logic.
Read:
const input = await ailaflow.readVariable('$input');
const startedBy = await ailaflow.getStartedBy();
Validate the action and write:
await ailaflow.writeVariable('$action', input.action);
Dispatch:
switch (input.action) {
case 'add_expense':
break;
case 'remove_expense':
break;
case 'show_summary':
break;
case 'show_entries':
break;
default:
throw new Error(`Unsupported action: ${input.action}`);
}
add_expense
Validate:
- user exists;
- user is not the starter;
- amount is valid and positive;
- split is
50:50or0:100.
Generate an expense ID.
Calculate:
amount_minor
owed_minor
pair_key
Write:
- one
#expensesrow; - payer movement with
+owed_minor; - other-user movement with
-owed_minor.
Prepare $add_result, for example:
{
"expense_id": "expense-id",
"payer": "@alice",
"bill_with": "@bob",
"amount": 120.5,
"owed": 60.25,
"currency": "PLN",
"split": "50:50",
"description": "Dinner"
}
Set:
$notification_users = "@alice or @bob"
Use lowercase or.
Prepare $notification_text, for example:
@alice added a 120.50 PLN expense with @bob in expense_tracker.
remove_expense
Read:
const expense = await ailaflow.tryReadTable('#expenses', input.expense_id);
Fail if it does not exist or is already removed.
Anyone may remove it.
Read the two movement rows using their deterministic IDs.
Rewrite them with:
status = "removed"
while preserving their other fields.
Rewrite the expense with:
status = "removed"
removed_at = current timestamp
removed_by = startedBy
Prepare $remove_result with the expense ID, participants, amount, currency, and remover.
Set:
$notification_users = "@alice or @bob"
using the original expense participants.
Prepare a message such as:
@charlie removed a 120.50 PLN expense between @alice and @bob in expense_tracker.
show_summary
Read only active movements belonging to the starter:
await ailaflow.readTablePage('#expense_movements', {
page: 1,
where: {
user: { $eq: startedBy },
status: { $eq: 'active' }
}
});
Continue through pages until:
hasMore === false
Group rows by with_user and sum amount_minor.
Discard zero balances.
Positive total:
other user owes you
Negative total:
you owe other user
Prepare:
{
"user": "@alice",
"currency": "PLN",
"total_owed_to_you": 120.5,
"total_you_owe": 42.0,
"net": 78.5,
"balances": [
{
"user": "@bob",
"amount": 60.25,
"direction": "owes_you"
},
{
"user": "@john",
"amount": 42.0,
"direction": "you_owe"
}
]
}
Sort balances by username.
Do not send notifications.
show_entries
Normalize and validate $input.user.
Use page 1 when omitted. A supplied page must be a positive integer.
Calculate:
const pairKey = createPairKey(startedBy, requestedUser);
Read:
const result = await ailaflow.readTablePage('#expenses', {
page,
pageSize: 50,
orderBy: 'created_at',
ascending: false,
where: {
pair_key: { $eq: pairKey }
}
});
Do not filter on status. Include active and removed history.
Prepare:
{
"user": "@alice",
"with_user": "@bob",
"currency": "PLN",
"page": 1,
"page_size": 50,
"has_more": true,
"entries": [
{
"expense_id": "expense-id",
"payer": "@bob",
"bill_with": "@alice",
"amount": 80,
"owed": 40,
"split": "50:50",
"description": "Taxi",
"status": "active",
"created_at": "2026-09-15T21:00:00.000Z"
}
]
}
Set:
has_more: result.hasMore;
Convert money back from minor units before returning.
Do not send notifications.
Branch
After Script, add a Branch using:
$action
Create exactly:
add_expense
remove_expense
show_summary
show_entries
Workflow:
Start
↓
Script
↓
Branch($action)
│
├─ add_expense
│ Notification
│ Finish($add_result)
│
├─ remove_expense
│ Notification
│ Finish($remove_result)
│
├─ show_summary
│ Finish($summary_result)
│
└─ show_entries
Finish($entries_result)
Each branch has its own Finish step.
Do not add a common Finish after Branch.
Notifications
For add_expense and remove_expense, configure Notification with:
User expression:
$notification_users
Notification text:
$notification_text
The generated expression must use lowercase or:
@alice or @bob
Do not notify for show_summary or show_entries.
Start form
Create a responsive Start form with:
Add expense
Remove expense
Show summary
Show entries
For Add expense, show:
- user;
- amount with configured currency;
- split selector;
- optional description.
Explain:
50:50 — I paid and the other user owes half.
0:100 — I paid and the other user owes the full amount.
For Remove expense, show:
- expense ID.
For Show summary, no additional fields.
For Show entries, show:
- user;
- page, default
1.
Hide irrelevant fields.
Validate inputs, use type="button", and call ailaflow.submitForm() inside try/catch.
The form may lowercase usernames, but the Script must normalize them again.
Finish forms
Create separate responsive Finish forms.
Add expense
Show:
- expense added;
- description;
- total amount;
- split;
- how much the other user owes.
Remove expense
Show:
- expense removed;
- amount;
- participants;
- who removed it;
- confirmation that balances changed.
Summary
Show:
- currency;
- total owed to the user;
- total user owes;
- net;
- individual balances.
If empty:
You're all settled up.
Entries
Show:
- both users;
- currency;
- newest entries first;
- payer;
- amount;
- split;
- owed amount;
- description;
- active/removed status;
- date.
Clearly distinguish removed entries.
If empty:
No expenses found between you and @user.
At the bottom show:
Page 1
More entries available
when has_more is true, otherwise:
Page 1
No more entries
Do not load subsequent pages from the Finish form. Another /expense_tracker execution requests the next page.
Implementation rules
- Ask for currency once while creating the process.
$inputis the only start variable.- Create a custom Start form.
- Normalize supplied usernames to lowercase.
- Validate supplied users with
userExists(). - User expressions must use lowercase
and/or. - Multiple notification recipients use
or, for example@alice or @bob. - Each expense involves two users; the tracker supports any number of users overall.
- The starter always pays newly added expenses.
- Support only
50:50and0:100. - The payer absorbs an odd minor unit for
50:50. - Anyone may remove an active expense.
- Never physically delete history.
- Store money in integer minor units.
- Store two signed movement rows per expense.
- Use canonical
pair_keyfor history. - Do not maintain mutable aggregate-balance rows.
- Summary reads only the starter’s active movements and processes all matching pages.
- History returns 50 records per page and
has_more. - Use Branch for action-specific behavior.
- Each action has its own Finish step and form.
- Notify affected users only when balances change.
- Keep Start and Finish forms simple, consistent, and responsive.