The starting point
Most business software assumes that people want to navigate a maze of screens. For Lazizah ERP, the goal was different: let an operator describe the work in plain language and let the system turn that intent into a reliable workflow.
The hard part was not adding a chat box. It was designing a boundary between flexible language and strict business operations.
Where the system does real work
The assistant can help with inventory management, invoice generation, ledgers, and day-to-day business workflows. Each request is translated into a structured action, checked against the user and the current business state, and then executed through the same domain services used by the rest of the product.
That separation matters. The model can suggest what should happen, but it should not be the source of truth for balances, permissions, or stock counts.
Natural language is the interface. The business rules still belong to the application.
The useful constraint
A good AI feature does not remove structure. It hides unnecessary structure from the user while preserving it underneath.
For this project, that meant investing in clear tools, explicit validation, and readable activity history. The result feels conversational without becoming mysterious: users can ask for an outcome, inspect what happened, and correct it when needed.
What I would carry forward
- Keep model output close to typed, testable application code.
- Treat retrieval as product context, not as a replacement for domain logic.
- Make every important action observable and reversible where possible.
- Optimize for the operator's next decision, not for impressive model output.
The most satisfying part of the work was seeing a complicated workflow become smaller for the person using it, while remaining rigorous for the system underneath.
The architecture behind the conversation
The conversational interface is only the front door. The reliable part is the typed command layer underneath it. An intent such as “show me unpaid invoices from last month” becomes a read operation with explicit filters. An intent such as “create an invoice” becomes a command that validates customer, line items, permissions, and accounting rules before writing anything.
flowchart LR
A[User language] --> B[Intent and entity extraction]
B --> C[Typed command or query]
C --> D[Permission and business validation]
D --> E[ERP domain service]
E --> F[(Inventory, invoices, ledgers)]
E --> G[Activity history]
G --> H[Human-readable response]
Why the boundary matters
The model should never calculate a balance from a partial context window or decide that a user has permission because a prompt sounded confident. It can choose a tool and fill a typed argument, but the domain service remains the source of truth.
type CreateInvoiceCommand = {
customerId: string;
lines: Array<{ productId: string; quantity: number; unitPrice: number }>;
};
async function createInvoice(command: CreateInvoiceCommand, actor: Actor) {
await permissions.assertCanCreateInvoice(actor);
const input = invoiceSchema.parse(command);
return accounting.createInvoice(input);
}
What I would measure
The important metrics are not only model latency or response length. I would measure the percentage of requests resolved without correction, the number of unsafe actions blocked by validation, the time saved on recurring workflows, and how often users leave the conversation for a traditional screen.
The product succeeds when language removes navigation cost while the underlying accounting behavior stays as rigorous as it was before.