Skip to content

Steel architecture and lifecycle

Steel scripts define one computed value for one table column. They are registered with the server, fetched by the client, executed by the client for immediate form feedback, and executed again by the server whenever row data is saved.

The client calculation is a preview. The server calculation is authoritative.

Use a table script when a column must be derived consistently from the current row, a linked row, or a related collection. Typical uses include:

  • multiplying a quantity by a unit price;
  • calculating net, VAT, and gross totals;
  • copying or classifying a value with conditions;
  • aggregating related lines into a document total;
  • producing a controlled contribution to a quantity-ledger column.

A script is not a general server hook. It cannot run arbitrary SQL, call services, import modules, or mutate tables. The only supported write-like effect is a declared quantity-add or quantity-subtract contribution.

script author
│ PostTableScript
server validates, transforms, analyzes, and stores the script
│ GetTableScripts
client registers the script and its dependency allowlist
├── current-row inputs come from the open form
└── HydrateScriptDependencies supplies declared external inputs
client evaluates for immediate feedback
│ PostTableData or PutTableData
server rebuilds the context and evaluates the same stored script
├── matching submitted value → commit
└── different or missing value → reject

There are therefore three separate concerns:

Concern Owner Purpose
Script definition TableScript gRPC service Validate and store calculation rules
Interactive calculation Client Steel runtime Recalculate a form immediately as input changes
Persistent enforcement TablesData gRPC service Recalculate inside the data transaction and reject incorrect values

PostTableScript binds a script to a table_definition_id and a target column. The last expression returned by the script is the target value.

(money-mul
(steel_get_column "invoice_line" "unit_price")
(steel_get_column "invoice_line" "quantity"))

If this script targets line_total, callers must send line_total when inserting the row. The server does not silently add a missing computed field to the request. It verifies that the client-supplied value equals the server result.

The server stores two linked records:

  • table_scripts stores the target, target type, transformed script, and description;
  • script_dependencies stores every permitted column read, related aggregate, and quantity effect.

The script and dependencies are written in one transaction. Column aliases are translated to stable physical names for storage. GetTableScripts translates them back to current public aliases, so renaming an alias does not require rewriting every script.

Before storing a script, the server checks:

  1. The table exists and is not a protected system table.
  2. The script is a non-empty S-expression and Steel can parse it.
  3. The expanded program contains only allowlisted forms and functions.
  4. The target column exists, is writable, and has a supported type.
  5. The target does not already contain data.
  6. Every database function uses literal names and references an existing column.
  7. Linked-row reads resolve through an explicit, unambiguous link.
  8. Related aggregates resolve through an unambiguous relationship path.
  9. Math, money, and quantity-ledger operations are type-safe.
  10. The transformed script and normalized dependencies can be committed together.

Source arithmetic such as +, *, and / is transformed to exact-decimal operations before storage. GetTableScripts returns this validated, transformed form.

The server builds a request-scoped context containing only declared values. Registered functions such as steel_get_column read from that context rather than querying arbitrary data from inside the VM.

The synchronous Steel VM runs away from asynchronous runtime worker threads. Engines are reused from a bounded pool, request context is cleared after each run, and per-run decimal precision state is reset.