Skip to main content

Server Plugin Step Registration

Schema id: plugin-step@1 Machine-readable schema: plugin-step.schema.json

The canonical registration wrapper for a Server Plugin Step (Section 6.6) — server-side logic that runs when a record on an entity is created, updated, or deleted. The wrapper says when and how the logic runs; a BS Lang implementation additionally carries the script inline, which is validated by the BS Lang grammar in addition to this schema. A registration is validated identically whether it comes from a designer, an API call, an AI assistant, or a Solution import.

Shape​

FieldTypeRequiredNotes
logicalNamestringyesPublisher-prefixed, lower snake-case.
displayNamestringyesHuman-readable step name.
publisherPrefixstringyesThe owning publisher's prefix, which logicalName must be namespaced by — not the watched table's. A standard table such as account is deliberately unprefixed, and validating against it made a step on one impossible to name.
entitystringyesThe entity whose writes trigger the step.
messagestringyesCreate, Update, or Delete.
stagestringyesPreValidation, PreOperation, or PostOperation.
executionModestringyesSynchronous (inline: whoever saved waits for it — inside the write's transaction at PreValidation/PreOperation, after the commit at PostOperation) or Asynchronous (queued through the outbox and run after the write commits). An asynchronous step must be PostOperation: it runs after the record is already saved, so it has nothing left to refuse.
rankintegeryesExecution order among steps on the same entity + message + stage; lower runs first.
isEnabledbooleanyesA disabled step is stored but never executed.
implementationobjectyesExactly one implementation (see below).

Implementation​

FieldTypeRequiredNotes
typestringyesBsLangScript or DotNetAssembly.
scriptstringwhen BsLangScriptThe BS Lang source.
assemblyReferencestringwhen DotNetAssemblyThe uploaded assembly's storage key.
typeNamestringwhen DotNetAssemblyThe plugin type to instantiate.

Stages​

  • PreValidation — runs before the operation; coarse shape/security checks.
  • PreOperation — runs just before the write; may change field values on the record or reject the whole operation.
  • PostOperation — runs just after the write; for cascading or dependent changes.

A step rejects an operation by calling throwValidationError("reason"), which stops the write and returns the reason to the caller.

Example​

{
"logicalName": "par_invoice_reject_negative",
"displayName": "Reject negative invoice total",
"entity": "par_invoice",
"message": "Create",
"stage": "PreOperation",
"executionMode": "Synchronous",
"rank": 1,
"isEnabled": true,
"implementation": {
"type": "BsLangScript",
"script": "if (getValue(\"par_total\") < 0) { throwValidationError(\"Total cannot be negative.\"); }"
}
}

BS Lang script host functions​

A BsLangScript step reads and changes the record through these host functions:

FunctionPurpose
getValue("field")The current value of a field on the record being written.
setValue("field", value)Change a field's value (honoured only in PreOperation).
getPreValue("field")The field's prior value (updates and deletes).
throwValidationError("message")Reject the operation and roll it back.
message() / stage() / entityName()The current operation, stage, and entity.
userId() / depth()The triggering user and the recursion depth.

Reaching other tables​

The functions above see only the record being written. These reach the rest of the environment, and every one of them runs as the user who triggered the step — a script is not a way around a permission, and a step whose caller cannot read a table gets nothing back from it.

FunctionPurpose
getRecord("table", id)One record as an object, or empty if it is not there. .id is on it.
queryRecords("table", "field", value)The rows where a field equals a value, as a list of objects. Capped at 200.
runQuery({ … })Runs a full parsware-query@1 document — nested and/or filters with ten operators, joins, sorts, paging, or an aggregate that answers groups. The same query language Advanced Find emits and a saved query stores. Rows capped at 200; an aggregate is not. A joined column reads under its alias (row.acct.par_industry), and a link's 'from' must be quoted because from is a BS Lang keyword.
runSavedQuery("name")Runs a saved query by logical name. The complexity lives in a designer and travels in the solution, so the script only names the question.
countRecords("table", "field", value)How many there are, without fetching them — a real COUNT, so it is right past the 200-row cap that bounds queryRecords.
createRecord("table", { field: value, … })Creates a record and answers its id.
updateRecord("table", id, { field: value, … })Changes the fields named, and only those.
deleteRecord("table", id)Deletes a record.

A write made this way runs the ordinary rules, so a step registered on the table being written will run in turn. That nesting is bounded at a depth of eight and depth() tells a script where it is — but the limit is a backstop, not a design. A step that writes the table it watches will trigger itself, and the fix is a condition or a different table.

Importing shared logic​

A step's script may import a solution script by name, exactly as a Client Business Rule does, so a rule enforced by several steps is written once:

import 'par_approval_rules';

if (getValue('par_total') > par_approval_rules.limit()) { … }

Both quote styles work. The module must be a solution script in the same environment; anything else fails when the step is saved rather than when it runs.