Skip to main content

DSL Reference

Complete reference for the DSL (Domain Specific Language) for business rules in Fyso.

General Structure

{
"type": "compute | validate | action",
"triggers": ["campo1", "campo2"],
"triggerType": "field_change | before_save | after_save | on_load",
"compute": { ... },
"validate": [ ... ],
"transform": { ... },
"actions": [ ... ]
}

Compute

Calculates values automatically. Supports several formats:

Simple Formula (shorthand)

{
"compute": {
"total": "cantidad * precio"
}
}

The shorthand is internally normalized to:

{
"compute": {
"total": { "type": "formula", "expression": "cantidad * precio" }
}
}

Explicit Formula

{
"compute": {
"iva": { "type": "formula", "expression": "subtotal * 0.21" },
"total": { "type": "formula", "expression": "subtotal + iva" }
}
}

Conditional

Calculates a value based on conditions:

{
"compute": {
"descuento": {
"type": "conditional",
"conditions": [
{ "when": "cantidad >= 100", "then": "0.15" },
{ "when": "cantidad >= 50", "then": "0.10" },
{ "when": "cantidad >= 10", "then": "0.05" }
],
"default": "0"
}
}
}

Lookup

Looks up a value in another entity:

{
"compute": {
"precio_unitario": {
"type": "lookup",
"entity": "productos",
"matchField": "id",
"matchValue": "producto_id",
"resultField": "precio"
}
}
}
PropertyTypeDescription
entitystringEntity to search in
matchFieldstringField in the target entity to match against
matchValuestringField in the current record containing the value to search for
resultFieldstringField in the target entity whose value to return

Aggregate

Aggregates values from multiple records of another entity:

{
"compute": {
"total_lineas": {
"type": "aggregate",
"entity": "lineas_factura",
"aggregateOp": "sum",
"aggregateField": "subtotal",
"filter": { "factura_id": "id" }
},
"cantidad_items": {
"type": "aggregate",
"entity": "lineas_factura",
"aggregateOp": "count",
"filter": { "factura_id": "id" }
}
}
}
PropertyTypeDescription
entitystringEntity to aggregate
aggregateOpstringOperation: "sum" or "count"
aggregateFieldstringField to sum (required for sum)
filterobjectFilter: { target_field: "current_field" }

Validate

Array of validation rules:

{
"validate": [
{
"id": "precio_positivo",
"condition": "precio > 0",
"message": "El precio debe ser mayor a cero",
"severity": "error",
"field": "precio"
}
]
}
PropertyTypeRequiredDescription
idstringYesUnique identifier for the validation
conditionstringYesBoolean expression that must be true
messagestringYesError message if the condition is false
severitystringYes"error" (blocks save), "warning", "info"
fieldstringNoField to associate the error with in the UI

Transform

Transforms field values:

{
"transform": {
"nombre": { "type": "uppercase" },
"email": { "type": "lowercase" },
"descripcion": { "type": "trim" },
"precio": { "type": "round", "decimals": 2 }
}
}
TypeDescription
uppercaseConverts to uppercase
lowercaseConverts to lowercase
trimRemoves leading and trailing whitespace
roundRounds to N decimal places

Actions

Side effects that execute after saving. Actions run sequentially and can share data through the execution context.

Updates a record in a different entity:

{
"actions": [
{
"type": "update_related",
"entity": "pedidos",
"recordId": "pedido_id",
"data": {
"total": {
"type": "aggregate",
"entity": "lineas",
"aggregateOp": "sum",
"aggregateField": "subtotal",
"filter": { "pedido_id": "pedido_id" }
}
}
}
]
}

update_record

Updates fields on the current record. Unlike update_related, this does not require entity or recordId — it targets the record that triggered the rule.

{
"type": "update_record",
"fields": {
"estado": "aprobado",
"aprobado_por": "$ctx.approver_name"
}
}
PropertyTypeRequiredDescription
fieldsobjectYesField→value pairs to set on the current record

Values can reference execution context variables using $ctx.* syntax. This is useful for chaining actions — for example, storing an AI classification result and then writing it back to the record.

ai_call

Invokes an AI model as a rule action. Useful for classification, extraction, summarization, or any text generation task triggered by record changes.

{
"type": "ai_call",
"prompt": "Classify this support ticket: {{descripcion}}",
"system_prompt": "You are a ticket classifier. Respond with exactly one of: bug, feature, question",
"model": "gpt-4o-mini",
"temperature": 0.3,
"max_tokens": 100,
"store_result_as": "$ctx.classification"
}
PropertyTypeRequiredDescription
promptstringYes (unless prompt_template)The prompt to send. Supports {{field}} substitution from the current record.
prompt_templatestringNoSlug of a reusable prompt template (resolves from _fyso_ai_templates)
system_promptstringNoSystem prompt for the AI call
system_prompt_templatestringNoSlug of a system prompt template
modelstringNoOverride model. Uses the tenant default if omitted.
temperaturenumberNo0–2. Uses the provider default if omitted.
max_tokensnumberNo1–32000
store_result_asstringNoStore the AI response in execution context. Must follow the format $ctx.<identifier>.

Guardrails:

  • A budget check runs before every AI call. If the tenant has exhausted its AI budget, the call is skipped.
  • A rate limit check runs before the budget check.
  • Failures are captured as error validations — the pipeline continues with remaining actions.
  • All calls are logged to _fyso_ai_call_logs.

webhook_send

Sends an HTTP POST request to an external URL. Useful for notifications, integrations, and event forwarding.

{
"type": "webhook_send",
"url": "https://hooks.example.com/notify",
"headers": {
"X-Api-Key": "my-token"
},
"payload": {
"ticket_id": "id",
"status": "estado",
"customer": "nombre_cliente"
},
"timeoutMs": 5000
}
PropertyTypeRequiredDescription
urlstringYesDestination URL. Supports {{field}} templates. SSRF protection enforced.
headersobjectNoCustom HTTP headers. Supports {{field}} templates.
payloadobjectNoKey→value payload. Values resolve from the current record at fire time. _record_id is injected automatically.
timeoutMsnumberNo100–30000 ms (default: 5000)

Behavior:

  • Always sends a POST request with a JSON body.
  • 1 automatic retry on failure (fire-and-forget — does not block the rule pipeline).
  • SSRF filter blocks private/internal IPs both at rule save time and at execution time.

Execution Context

Business rules can share data between actions using the execution context ($ctx). This is how you chain actions together — for example, calling an AI model and then writing the result back to the record.

The flow:

  1. An ai_call action stores its result via store_result_as: "$ctx.classification".
  2. A subsequent update_record action references $ctx.classification in its fields.
{
"actions": [
{
"type": "ai_call",
"prompt": "Classify: {{descripcion}}",
"system_prompt": "Respond with one of: bug, feature, question",
"store_result_as": "$ctx.classification"
},
{
"type": "update_record",
"fields": {
"categoria": "$ctx.classification"
}
}
]
}

Context variables are scoped to a single rule execution. They do not persist across separate rule triggers.

Optimistic Locking

All records include a _record_version field in API responses. When you update a record via the API and include _record_version in the payload, the server checks that the version matches the stored value. If it doesn't match (another update happened in between), the request is rejected with a version conflict error.

Actions triggered by business rules (update_related, update_record) bypass the version check to avoid conflicts during automated processing.

Allowed Operators

CategoryOperators
Arithmetic+, -, *, /
Comparison>, <, >=, <=, ==, !=
Logicaland, or

Allowed Functions

FunctionDescriptionExample
round(x, n)Rounds to n decimal placesround(total, 2)
coalesce(a, b)First non-null valuecoalesce(descuento, 0)
abs(x)Absolute valueabs(diferencia)
min(a, b)Minimummin(stock, pedido)
max(a, b)Maximummax(precio, precio_minimo)
floor(x)Round downfloor(cantidad)
ceil(x)Round upceil(horas)
len(s)String lengthlen(nombre)
upper(s)Uppercaseupper(codigo)
lower(s)Lowercaselower(email)
trim(s)Remove whitespacetrim(nombre)
now()Current date and timenow()
today()Current datetoday()