Business Rules
Business rules are the automation layer in oikapi. They fire at specific lifecycle points and can validate data, calculate fields, run scripts, trigger workflows, send notifications, and call external services.
Every rule has a trigger (when it fires), an optional condition (whether it fires), and one or more actions (what it does).
Rule types
Section titled “Rule types”The rule type is UI categorization metadata that helps organize rules in the builder. It does not change how a rule executes — behavior comes entirely from the trigger and actions.
| Type | Typical use |
|---|---|
validation | Block operations that don’t meet criteria |
calculation | Auto-compute field values |
field_default | Conditionally set field values |
script | Run custom JavaScript/TypeScript |
notification | Send notifications |
Triggers
Section titled “Triggers”| Trigger | When it fires |
|---|---|
before_validate | Before field validation runs |
before_create | After validation, before INSERT |
after_create | After INSERT, before the response is returned |
before_update | Before UPDATE |
after_update | After UPDATE |
before_delete | Before a (soft) delete |
after_delete | After a (soft) delete |
on_field_change | When a specific field changes |
scheduled | On a cron schedule |
recurrence | When a recurrence rule (RRULE) occurrence fires |
deadline | When an SLA / deadline threshold is crossed |
manual | On-demand via an action button |
webhook | When an inbound webhook arrives |
form_submission | After a public form submission creates a record |
email_inbound | When inbound email creates or updates a record |
http_endpoint | When a custom named API route is called |
app_event | When another application emits a subscribed event |
Actions
Section titled “Actions”Rules contain one or more actions that execute when conditions are met. The most common actions are below; advanced installations expose additional purpose-built actions (currency conversion, price resolution, stock movements, alerting, cross-app writes, and more).
Validate
Section titled “Validate”Block an operation with an error message:
{ "type": "validate", "config": { "condition": "gt(amount, 0)", "message": "Amount must be positive" }}Every other scoped action below spells its scoping key if; validate accepts either if or condition (they’re synonyms here) — supplying both with conflicting values is a configuration error.
Set field
Section titled “Set field”Set a field value, optionally only when an if condition holds:
{ "type": "set_field", "config": { "field": "status", "value": "approved", "if": "gte(amount, 10000)" }}Calculate
Section titled “Calculate”Compute a field from a formula:
{ "type": "calculate", "config": { "target_field": "total", "formula": "quantity * unit_price", "precision": 2 }}Copy field
Section titled “Copy field”Copy a value from a related record:
{ "type": "copy_field", "config": { "from_table": "customers", "from_field": "credit_limit", "to_field": "customer_credit_limit", "lookup_field": "customer_id" }}Execute script
Section titled “Execute script”Run a JavaScript script — either inline or by reference:
{ "type": "execute_script", "config": { "script_id": "<script-uuid>" }}You can also supply "script" with inline code instead of script_id.
Send notification
Section titled “Send notification”Send an email or SMS to a recipient:
{ "type": "send_notification", "config": { "channel": "email", "to": "$record.contact_email", "subject": "Your order $record.id was received", "body": "Thanks for your order." }}Field references in to, subject, and body are interpolated from the record. See Notifications for the richer scripting API.
Trigger workflow
Section titled “Trigger workflow”Start an approval workflow. Only allowed in after_* triggers (workflows run asynchronously):
{ "type": "trigger_workflow", "config": { "workflow": "expense_approval" }}Require approval (gate)
Section titled “Require approval (gate)”Block a write when a record enters a gated state carrying an over-threshold amount that has not been approved. This is a synchronous gate — it blocks the write, it does not route an approval request:
{ "type": "require_approval", "config": { "gate_field": "status", "gate_values": ["sent", "accepted"], "amount_field": "discount_percent", "threshold": 20, "approved_field": "discount_approved", "message": "Discounts over {threshold}% require approval" }}Request approval (routed)
Section titled “Request approval (routed)”Create a routed approval request after the write commits, without blocking it. Approvers are resolved by role, user, or an explicit list and resolve under any/all/majority mode:
{ "type": "request_approval", "config": { "approver_type": "role", "approver_role": "manager", "mode": "any", "title": "High-value order needs approval" }}Conditions
Section titled “Conditions”Rules can have a condition using the same filter syntax as queries. The rule’s actions run only when the condition evaluates true:
{ "condition": "and(eq(status, 'submitted'), gt(amount, 5000))", "actions": [...]}A rule’s top-level condition is honored end-to-end, including for rules installed as part of a package — it round-trips through the install payload and is evaluated the same as a directly-created rule’s condition. Scope a package rule with its top-level condition rather than re-deriving the same predicate inside every action.
Field-change triggers
Section titled “Field-change triggers”Monitor specific field changes with fine-grained control:
{ "trigger": "on_field_change", "trigger_config": { "watched_fields": ["status"], "change_type": "to", "to_value": "approved" }}Change types: any, to, from, from_to, comparison. The comparison type supports increase_by, decrease_by, gt, lt, gte, lte.
Async execution
Section titled “Async execution”Rules can run in the background via the job queue:
{ "async": true, "async_priority": 100, "async_delay_seconds": 60}Failed async rules retry with exponential backoff.
Scheduled rules
Section titled “Scheduled rules”Run rules on a cron schedule:
{ "trigger": "scheduled", "trigger_config": { "cron": "0 2 * * *", "timezone": "America/Denver" }}Scheduled rules have no single-record context — they run application-wide.
Rule lifecycle
Section titled “Rule lifecycle”A rule must be active to execute. The lifecycle states are:
| Status | Meaning |
|---|---|
draft | Editable, not executing |
pending_review | Awaiting approval |
active | Executing on its table |
disabled | Manually disabled |
Permissions and boundaries
Section titled “Permissions and boundaries”- Permission mode:
user(default, runs with the user’s permissions),creator(runs with the rule creator’s permissions), orelevated(admin-only, bypasses permission checks). - Application boundaries: Rules can only access tables in their own application plus system tables. To access tables in another application, an active
data_accessintegration must exist between the two applications. - Loop protection: Rule chains are bounded by a maximum execution depth, and each script is bounded by a maximum number of cross-table operations.
Cross-app access
Section titled “Cross-app access”Cross-app table access is controlled by data_access integrations between applications — not by a field on the rule itself. When a rule tries to read or write a table in another application, the platform checks for an active data_access integration from the rule’s application to the target application. If no such integration exists, the operation is denied.
Creating rules
Section titled “Creating rules”Business rules are created and managed through the system tables API:
POST /api/apps/system/tables/business_rules/records{ "name": "Validate positive amount", "table_id": "<table-uuid>", "application": "my-app", "scope": "table", "trigger": "before_create", "rule_type": "validation", "active": true, "actions": [ { "type": "validate", "config": { "condition": "gt(amount, 0)", "message": "Amount must be positive" } } ]}