Developer hub

The BOSL Language

Business Operating System Language. Language Specification, version bosl/v1.

Introduction

BOSL is the language in which applications for the Business Operating System are written. A BOSL program is a graph. It declares the states an application can be in, the transitions between them, the interface people see in each state, who may see and do what, and the operations and journeys the application can perform. A BOSL program running on the platform is an app. Lead Director and Subscription Director are both apps implemented in BOSL.

BOSL is declarative. A program has no procedures of its own. It states what exists and what is allowed, and a platform interprets it, so the same language describes the behavior, the interface and the permissions of an application, and all of them can be examined before anything runs. Work that acts on the world is an operation, defined outside the language and reached by name. Work that an agent carries out step by step is a journey, which is a smaller graph with its own form.

BOSL takes its syntax from JSON. Every BOSL program is a JSON text, so any JSON parser can read one. BOSL adds no syntax of its own. What it adds is a required structure, and rules that give that structure a meaning.

1. Scope

This specification defines the syntax of BOSL programs and journeys, which share one parser, the rules that determine whether they are valid, and their meaning when they run.

It does not define any particular application, the operations a program may name, how a platform stores or transmits programs, how a client finds operations, or any bound on size or duration.

2. Conformance

A conforming program is a JSON text that matches the syntax in this specification and satisfies the rules in section 21.

A conforming processor accepts conforming programs, rejects every other program before any part of it runs, and carries out accepted programs as section 22 requires. A processor MAY refuse a conforming program for a reason outside this specification, such as a resource bound or an operation the author cannot use, provided it does so before any part of it runs.

The key words MUST, MUST NOT, REQUIRED, SHOULD and MAY are to be interpreted as described in RFC 2119.

3. Normative references

4. Terms and definitions

program
A BOSL graph: a JSON text whose value is a program object.
app
A program running on the platform.
platform
The system that interprets programs. A processor is the part of it that validates and executes them.
organization
The party for which an app is installed.
record
An item an app tracks, such as a lead or a subscriber.
state
A named place a record or a user can be, declared in node_types.
transition
A declared move from one state to another.
role
A named set of permissions held by a user.
plugin
A unit of capability declared by a program.
operation
A named unit of work defined outside the program.
contract
The published declaration of what an operation accepts, produces and requires.
journey
A graph of steps carried out by an agent and the service.
agent
The party that carries out client nodes of a journey.
author
The party that registers a journey, and on whose behalf its runs act.
run
One execution of a registered journey.
visit
One entry into a journey node during a run.

5. Notation

BOSL's lexical grammar is that of JSON, except that an object MUST NOT repeat a member name. Its syntactic grammar is stated here over JSON values, not over characters. A member whose name begins with an underscore is an annotation: it is valid in any object and has no effect on execution. In an object production, ... after other members means an implementation may accept further members there. In { name : X, ... } it means any number of members of that form.

FormDenotes
{ a : X, b : Y? }A JSON object with exactly these members, in any order. A member followed by ? is optional. An object MUST NOT have any other member.
[ X ]A JSON array in which every element is an X.
[ X ]+A JSON array of at least one X.
X | YEither X or Y.
"text"That exact JSON string.
/pattern/A JSON string matching the regular expression in full.
string, object, integer, boolean, trueThe corresponding JSON value. A string in a production MUST NOT be empty.
positive-integerAn integer greater than zero.
nameAny member name chosen by the author.
valueAny JSON value.
schemaA JSON object that is a JSON Schema.

6. Program structure

A BOSL program is a single JSON object whose members are its sections, stored in a UTF-8 file with the extension .bosl. Six sections are required for a program to run.

program = { org_id : string, name : string, app_id : string,
            metadata : metadata, node_types : [ state ]+, fsm : fsm,
            description : string?, version : string?,
            ui_config : ui-config?, visibility_rules : visibility? }
SectionRequiredPurposeDefined in
org_idYesThe organization the program belongs to.section 7
nameYesThe application's name.section 7
app_idYesThe application's identity.section 7
metadataYesRoles, plugins and operations.section 13
node_typesYesThe states.section 8
fsmYesThe transitions.section 9
descriptionNoWhat the application is for.section 7
versionNoThe program's version.section 7
ui_configNoThe application's visual definitions.section 10
visibility_rulesNoWhich roles see which states.section 12

The interface is declared in each state (section 11). Journeys are declared by plugins or authored by clients (section 15). Sections may appear in any order.

The smallest program that can run has one state and no transitions:

{
  "org_id": "00000000-0000-0000-0000-000000000001",
  "app_id": "00000000-0000-0000-0000-000000000002",
  "name": "Smallest app",
  "metadata": { "roles": {}, "operational_metadata": { "known_operations": [] }, "plugins": {} },
  "node_types": [
    { "code": "home", "name": "Home", "is_entry_point": true, "is_exit_point": false }
  ],
  "fsm": { "transitions": [] }
}

What a program needs in order to run is stated in section 22.

7. Identity

org_id and app_id identify the organization and the application. name is the title people see. description and version are descriptive and have no effect on execution. A program belongs to one application, and each application has one program for each organization it is installed for.

8. States

node_types declares the states of the application. A state is both a place a record can be and a screen a user can see. An app's sign-in screen, its work queue and a lead's stage in a pipeline are all states.

state = { code : state-code, name : string,
          category : string?, description : string?,
          status_label : string?,
          is_entry_point : boolean?, is_exit_point : boolean?,
          is_default : boolean?, is_external_entry : boolean?,
          is_positive_goal : boolean?, is_negative_goal : boolean?,
          auth_required : boolean?,
          queue_assignment : { role : [ string ] }?,
          schema : schema?, tabs : tabs?,
          ui_tree : component?, attributes : object? }

state-code = /^[a-z][a-z0-9_]*$/
MemberMeaning
codeThe state's name within the program.
name, status_labelWhat people see.
categoryA grouping of states, such as intake or retention.
is_entry_pointThe application or a record can begin here.
is_exit_pointThe record's path ends here.
is_defaultThe state used when none is specified.
is_external_entryRecords can arrive here from outside the app.
is_positive_goal, is_negative_goalReaching this state counts as a success or a loss.
auth_requiredWhether a signed-in user is needed.
queue_assignmentThe queues in which each role sees records in this state.
schemaA JSON Schema for the data a record holds in this state.
tabsThe tabs shown in this state.
ui_treeThe interface for this state (section 11).
attributesFurther properties of the state, which may also hold its ui_tree.

9. Transitions

fsm holds the transitions. A record or user moves between states only by a declared transition.

fsm        = { transitions : [ transition ], entry_point : state-code?, ... }

transition = { code : string, name : string?,
               from_node_type_code : state-code,
               to_node_type_code : state-code,
               trigger : "manual" | "system"?,
               conditions : value?, actions : value?,
               requires_plugin : string?,
               action_metadata : action-metadata? }

action-metadata = { action_type : action-type,
                    role_permissions : { role : boolean }?,
                    system_actor_allowed : boolean?,
                    label_template : string?, icon : string?, color : string?,
                    priority : integer?, endpoint : string?,
                    requires_input : boolean?, form_schema : schema?,
                    plugin_id : string?, ... }

A transition with the trigger system is taken by the platform. Otherwise it is offered to a user as an action, shown with its label_template, icon and color and ordered by priority. role_permissions states which roles may take it. system_actor_allowed states whether the platform may take it on a role's behalf. A transition that names requires_plugin is available only when that plugin is enabled.

action_type states how the transition is carried out:

Action typeMeaning
transitionMove the record immediately.
transition_with_formCollect a form, then move the record.
navigationMove the user to another state.
external_actionPerform an action on the user's device, then move the record.
pluginHand the action to a plugin.
oauthSign the user in or connect a provider.

10. Visual definitions

ui_config declares the values an interface refers to by name, so that an application's appearance is defined in one place.

ui-config = { colors : object?, styles : object?, typography : object?,
              icon_library : string?, app_icon : string?, ... }

colors is a tree of named colors and styles a set of named style objects. A style may refer to colors and to other styles. These values are available to every interface in the program.

11. User interface

The interface of a state is a tree of components, declared in its ui_tree. BOSL states what to show and what it does. The platform renders it on each device. An app has no interface code of its own.

component-name = "View" | "SafeAreaView" | "ScrollView" | "Text" | "Icon" | "Image"
               | "TouchableOpacity" | "Pressable" | "TextInput" | "Picker" | "Switch"
               | "FlatList" | "ActionList" | "Modal" | "ActivityIndicator"
               | "Sparkline" | "SelectablePriceGrid"

component = { component : component-name,
              style : style?, dynamicStyle : style?,
              value : value?, props : object?,
              children : [ component ]?,
              action : action?,
              condition : value?, visible_if : value?,
              data : ui-reference?, item_template : component?,
              emptyComponent : component? }

style     = object | ui-reference
ui-reference = /^\$[a-z_]+(\.[A-Za-z0-9_.-]+)?$/

11.1 Components

ComponentPurpose
View, SafeAreaViewContain and lay out children.
ScrollViewContain content that scrolls.
TextShow value as text.
Icon, ImageShow an icon or an image.
TouchableOpacity, PressablePerform an action when pressed.
TextInput, Picker, SwitchCollect a value from the user.
FlatListRepeat item_template for each element of data, or show emptyComponent when there are none.
ActionListShow the actions available to the user in this state.
ModalShow content over the current screen.
ActivityIndicatorShow that work is in progress.
Sparkline, SelectablePriceGridShow a trend, or a choice of prices.

11.2 References

A string beginning with $ refers to a value. Interface references are distinct from the journey references of section 17.

ReferenceValue
$colors.pathA color from ui_config.
$styles.nameA style from ui_config.
$node.data.fieldA field of the current record.
$node.status_label, $node.status_colorThe display status of the current record.
$node.itemsThe items of the current state.
$item.fieldA field of the current list element, also written {item.field}.
$actionsThe actions available in this state.
$metadata.keyA value from the state's metadata.

A style object may combine styles with $merge, whose value is a list of styles.

11.3 Actions

action = { action_id : string, action_type : string?,
           endpoint : string?, method : string?,
           params : object?, payload : object?,
           navigate : value?, target : value?,
           confirm : value?, collect_form_data : boolean?, ... }

An action is what a component does when used. action_id names it. It may call an endpoint with a method, navigate, or collect the values of the surrounding form with collect_form_data. confirm asks the user first. Whether a user may perform an action is decided by the platform when it is performed (section 22.2), not by the interface showing it.

11.4 Conditional display

A component with condition or visible_if is shown only when that value is present and true.

11.5 Native platform apps

The interface of a native platform app is rendered by the platform's React Native build, not by a browser. A native platform app MUST conform its interface to what that build supports:

A processor MUST reject a native platform app whose interface uses a component the build does not support.

12. Visibility

visibility_rules states which roles can see each state.

visibility = { state-code : [ role ] | { role_based : boolean, visible_for : [ role ] }, ... }

With role_based false, the state is visible to everyone who can reach it.

13. Metadata

metadata declares who uses the application and what it can do.

tabs     = [ { id : string, label : string, ... } ]+ | ui-reference
role-definition = { label : string?, description : string?,
                    agent_capabilities : [ string ]?, screen : value?, ... }

metadata = { roles : { role : role-definition },
             plugins : { string : plugin },
             operational_metadata : { known_operations : [ string ] },
             tabs : tabs?, ... }

known_operations lists the operations the program uses. tabs defines the app's one tab bar.

13.1 Roles

roles is the only place a program defines roles. A role definition gives its label and description, the screen a user with that role starts on, and agent_capabilities: the capabilities an agent acting in that role holds. An operation's contract states the capability it requires (section 14).

13.2 Plugins

A plugin is a unit of capability, such as a provider connection or a payment service.

plugin = { id : string, name : string, description : string?,
           enabled : boolean?, category : string?,
           operation_contracts : { operation-name : contract }?,
           mcp_settings_schema : schema?, settings_sections : value?,
           connection : object?, workflow : object?, ... }

A plugin's operation_contracts publish its operations. mcp_settings_schema and settings_sections declare its settings. connection states how it connects to a provider. workflow declares its setup screens, in the same components as section 11. An app's capabilities are the enabled plugins of its program.

14. Operations

An operation is defined outside the language. BOSL has no way to express what an operation does. A program publishes an operation's contract through a plugin (section 13.2), and a journey names the operation. The contract states what that name means.

operation-name = string

Operation names are dotted, as in subscription-director.products.search. An operation belongs to a plugin, a plugin to an application, and an application is installed for an organization, so the set of operations available depends on the organization, the app and the user.

14.1 Contracts

A contract declares:

A program is checked against the contract and never against the operation's implementation.

14.2 Server operations

A contract declares whether its operation is a server operation. Only a server operation can be named by an operating node. Any other operation is called directly, outside the journey. A client that needs its result before a journey starts calls it first and passes the result as a journey input.

14.3 Binding classes

Every binding has a class: literal, journey_input, server_node_output, client_node_output, server_node_error or client_node_error. A contract states the classes it accepts for each input.

This is how an operation controls where its inputs may come from. An input that accepts only literal is fixed when the journey is written. An input that accepts client_node_output can receive an agent's attestation, and one that does not cannot.

14.4 Effects

Each contract declares the effect of its operation. A read observes state and changes nothing. Other effects include create, update, delete and communicate, and a contract may name a more specific one. A program cannot alter an operation's effect.

14.5 Approval

Approval in BOSL is data that flows through the graph. A client node attests it through its approval output, and a later operating node binds that output to an input of its operation:

{ "approval": { "from": "$nodes.review.outputs.approval" } }

An operation that requires approval MUST NOT run without it. Approval covers what the contract says it covers and nothing else.

14.6 Errors and recovery

An operation that fails produces an error object with a code, a message, and an indication of whether it may be retried. A contract may also give a recovery instruction for the agent.

An operation with an external effect MUST NOT be repeated automatically unless its contract permits it. An uncertain outcome MUST NOT be treated as a failure that is safe to repeat.

15. Journeys

A journey is a graph of steps carried out by an agent and the service. Where states and transitions describe how an app stands, a journey describes a piece of work from start to finish. A plugin may publish one, and a client may author one for a particular task.

A journey is a JSON object with five required sections and two optional ones.

journey = { identity : string, name : string, inputs : object,
            entry : node-code, nodes : [ node ]+,
            bootstrap : bootstrap?, controls : controls? }

identity names the journey. It is compared after normalization to Unicode Normalization Form C and MUST NOT contain control characters. name is a title for people to read. inputs holds the journey's input values, each a plain JSON value. entry names the node at which a run begins. Execution order comes only from entry and from each node's successors, so the order of nodes has no meaning.

A journey's hash is the Merkle sum of its node hashes. Each node's hash is the SHA-256 of its canonical JSON text. The node hashes are sorted, and adjacent pairs are hashed in turn until one remains. Only the contents of nodes take part, so identity, name and inputs do not change a journey's hash. Two journeys with the same hash are the same job.

16. Journey nodes

A journey has two types of node, named for the party that carries them out.

node         = client-node | server-node
server-node  = operating-node | terminal-node

client-node  = { code : node-code, type : "client",
                 instruction : instruction,
                 inputs : bindings, outputs : outputs,
                 successor, catch : branch?, max_visits : positive-integer? }

operating-node
             = { code : node-code, type : "server",
                 operation : operation-name,
                 inputs : bindings, outputs : outputs,
                 successor, catch : branch?, max_visits : positive-integer? }

terminal-node
             = { code : node-code, type : "server", terminal : true,
                 inputs : bindings, outputs : outputs,
                 catch : branch?, max_visits : positive-integer? }

successor    = next : node-code | transitions : branch
instruction  = { goal : string, message : string }
bindings     = { name : binding, ... }
outputs      = { name : schema, ... }
node-code    = /^[a-z][a-z0-9_]*$/

successor stands for exactly one of the two members it names. A node's code names it within the journey. max_visits is the number of times one run may enter the node. When it is absent, that number is one.

16.1 Client nodes

A client node is an instruction to the agent. goal states the purpose in a short identifying form and message states it in words. The node's resolved inputs accompany the instruction. Input names MUST NOT be goal, message, after_success or on_failure: the first two are the instruction itself, and the processor uses the last two to give the agent its actions for reporting completion and failure.

A client node's outputs are attestations, not data. An agent can state that something was done or agreed, but it cannot supply values for later steps through a client node. Output names are limited to these:

OutputAttestsSchema
approvalThe user approved.The attestation schema below.
confirmationThe user confirmed.The attestation schema below.
acknowledgmentThe user acknowledged.The attestation schema below.
failureThe step failed.A closed object requiring code, one of USER_STOPPED or CLIENT_STEP_FAILED, and message, with optional details of scalar values.

The attestation schema is this exact schema, in which confirmed can only be true:

{ "type": "object", "required": ["confirmed"],
  "properties": { "confirmed": { "const": true } },
  "additionalProperties": false }

16.2 Operating nodes

An operating node asks the service to perform an operation. Its inputs and outputs are determined by that operation's contract (section 14).

16.3 Terminal nodes

Terminal is not a third type of node. A terminal node is a server node that declares terminal : true, and reaching one ends the run. It has no operation and no successor.

A journey usually ends at a terminal node with empty inputs and outputs. A terminal node with outputs produces the outcome of the run. Either its output names equal its input names, or it has one output whose schema declares a property for every input and requires only inputs.

17. Journey bindings and references

A binding supplies the value of a node input.

binding   = { value : value }
          | { from : reference, optional : true? }

reference = /^\$journey\.inputs\.[A-Za-z_][A-Za-z0-9_]*$/
          | /^\$nodes\.[a-z][a-z0-9_]*\.outputs\.[A-Za-z_][A-Za-z0-9_]*$/
          | /^\$nodes\.[a-z][a-z0-9_]*\.error$/
          | /^\$outputs\.[A-Za-z_][A-Za-z0-9_]*$/
          | /^\$error\.[A-Za-z_][A-Za-z0-9_]*$/
          | /^\$event\.fields\.[a-z][a-z0-9_.]*$/

{ value : v } supplies v literally. { from : r } supplies the value of reference r. With optional : true, the input is omitted when r has no value.

ReferenceDenotesAppears in
$journey.inputs.nameA journey input.Bindings
$nodes.code.outputs.nameThat output from the latest successful visit to the node that produced it.Bindings
$nodes.code.errorThe error from the latest failed visit to the node.Bindings
$outputs.nameAn output of the current node.transitions
$error.fieldA field of the current node's error.catch
$event.fields.pathA field of the event being matched.A callback's match and dedupe_on

18. Journey conditions and branches

branch     = { cases : [ case ], default : node-code }
case       = { when : condition, next : node-code }

condition  = predicate
           | { all : [ predicate ]+ }
           | { any : [ predicate ]+ }

predicate  = { left : operand, op : comparator, right : operand? }
operand    = { from : reference } | { value : value }
comparator = "eq" | "neq" | "gt" | "gte" | "lt" | "lte"
           | "exists" | "not_exists"

Conditions do not nest. A predicate has a left and, except with exists and not_exists, a right.

An operand has a value if it is a literal, or if its reference has one. Predicates evaluate as follows:

ComparatorTrue when
existsleft has a value.
not_existsleft has no value.
eqBoth operands have values, and the values are equal.
neqBoth operands have values, and the values differ.
gt, gte, lt, lteBoth operands have values of an ordered type, and the ordering holds. Numbers and strings are ordered.

It follows that a comparison with a missing operand is false, including neq. all is true when every predicate is true. any is true when at least one is.

A branch evaluates its cases in array order and selects the next of the first case whose condition is true. If none is true, it selects default.

19. Events

A journey can wait for something to happen outside it. Its optional bootstrap section declares what it may wait for, and it waits at an ordinary operating node.

bootstrap     = { callbacks : [ callback ]+, activate_on : callback-code? }

callback      = { code : callback-code, source : source,
                  inputs : object, match : condition,
                  dedupe_on : [ event-field ]+?,
                  retry : retry?, dead_letter : dead-letter? }

retry         = { max_attempts : positive-integer, backoff : "exponential",
                  max_delay_seconds : positive-integer }
dead-letter   = { queue : "journey", retention_seconds : positive-integer }

callback-code = /^[a-z][a-z0-9_]*$/
source        = /^[a-z0-9_-]+(\.[a-z0-9_-]+)+$/
event-field   = /^\$event\.fields\.[a-z][a-z0-9_.]*$/

A callback names an event source and states, in match, which events it accepts. Its operands use the event reference of section 17. dedupe_on names the fields that identify a delivery, so a repeated delivery cannot advance a run twice. retry and dead_letter state what happens to a delivery that cannot be processed.

The operation bosl.callbacks.await waits for an event. It is defined by this specification and takes three inputs, each a literal. callback is REQUIRED and names a declared callback. ttl_seconds is OPTIONAL: a positive integer of seconds. Without it, the processor applies its own default. A processor may grant a shorter wait than requested. match is OPTIONAL: a condition that narrows the wait further. Its operands may refer to the event and to outputs of earlier nodes that every path to the wait has passed through. It produces a reference to the captured event, an object with kind equal to callback_event and an opaque reference. If the wait expires, the node fails with the code CALLBACK_WAIT_EXPIRED.

Starting a run establishes the point after which events are captured. A run captures each event at most once. With activate_on, which names a declared callback, each matching event starts a new run instead.

20. Journey controls

A journey's optional controls section states which roles may direct a run.

controls = { actions : [ action ]+ }
action   = { verb : verb, roles : [ role ]+ }
verb     = "start" | "stop" | "retire" | "complete" | "step" | "failed"
role     = /^[a-z][a-z0-9_-]*$/

An action states the roles required to perform a lifecycle verb. Every role in one action is required. Several actions for the same verb are alternatives. Roles govern who may direct a run. They do not change what the run's operations may do.

21. Static semantics

21.1 Programs

A program that matches the syntax above is valid only if it also satisfies these rules.

  1. State codes are unique within a program, and transition codes are unique within it.
  2. At least one state is an entry point.
  3. Every transition's from_node_type_code and to_node_type_code name a state.
  4. Every role named in a state, transition or visibility rule is defined in metadata.roles.
  5. A transition's requires_plugin names a plugin in metadata.plugins.
  6. Every interface reference resolves to a declared value.
  7. Every component names a component in section 11.1, and a native platform app conforms to section 11.5.

21.2 Journeys

A journey that matches the syntax above is valid only if it also satisfies these rules, which are decidable from the journey and the contracts it names, without running it.

  1. Node codes are unique within a journey, and callback codes are unique within bootstrap.
  2. entry, every next and every default name a node in the journey.
  3. Every node is reachable from entry, following successors and catch.
  4. Every cycle in the graph contains a node that declares max_visits. This makes every run finite. To repeat a cycle, each node on it needs a max_visits large enough for the repetitions, because a node without one can be entered only once.
  5. A $journey.inputs reference names a member of the journey's inputs.
  6. A $nodes output reference names a declared output of a node in the journey, and a $nodes error reference names a node in the journey.
  7. A binding uses only $journey and $nodes references. A condition in transitions uses only $outputs references, naming declared outputs of its node. A condition in catch uses only $error references, naming fields of the node's error. $event.fields references appear only in a callback and in the match of a wait.
  8. A predicate with exists or not_exists has no right. Every other predicate has one. A literal right is valid for the schema of its left, a referenced right has a compatible type, and an ordered comparison compares two numbers or two strings.
  9. An operating node names a server operation that is available to the author. It supplies every required input and no undeclared one, and each input uses a binding class the contract accepts. Each literal is valid for its input's schema, and each reference has a compatible schema.
  10. An operating node's outputs have the same names as its contract's outputs, with equivalent schemas.
  11. activate_on and the callback input of a wait name declared callbacks. A wait requires bootstrap.
  12. A journey contains nothing prohibited by section 23.

22. Execution semantics

22.1 Requirements to run

A program runs as an app only if it has all six required sections (section 6), it satisfies section 21, and it is installed for an organization.

22.2 States and transitions

Each record is in exactly one state. A user sees the interface of the state they are in, subject to visibility_rules. The actions offered are the transitions out of that state that the user's role may take and whose plugins are enabled.

A transition is checked when it is performed: the user's current role MUST permit it, or it MUST be a system transition. On success, the record is in the new state. A record moves only by a declared transition. Interface and agent requests get the same check, so what a person can do and what an agent can do are the same.

22.3 Journey registration

A journey runs only if it is valid (section 21.2), each operation it names is available to its author and ready, each event source it declares is ready, and it has been registered and explicitly started. Registration presents a journey to a processor, which validates it. Registration performs no effects.

A job has one current run. Registering a journey with the same hash as an existing job of the same author replaces that job: its current run ends, its record is kept, and the job keeps its identity. Each run is a distinct execution of the job. Spend against a job's budget carries across its runs. How long a run may last is set by the limits of the organization and its subscription.

22.4 Journey runs

A run has a current node and an ordered history of visits. Starting a run sets the current node to entry. The processor then repeats these steps:

  1. If the current node has been visited as many times as its max_visits allows, the run fails. Otherwise, record a visit.
  2. Resolve the node's bindings. If a binding that is not optional has no value, the run fails.
  3. If the node is a terminal node, compute its outcome and end the run.
  4. If it is a client node, give the instruction to the agent and wait. The agent reports completion or failure. The agent reports what happened but does not choose the next step.
  5. If it is an operating node, perform its operation.
  6. If the node succeeded, the next node is its next, or the result of evaluating transitions against its outputs.
  7. If the node failed and has a catch, the next node is the result of evaluating it against the error. If it has none, the run fails. A failure with the code USER_STOPPED never enters catch.

A run that has failed stays failed.

22.5 Authority

A run acts on behalf of its author. Whether an operation may run is determined by the author's current standing, not by the journey, and it is determined each time the operation runs. A journey cannot grant itself access, and a run's agent cannot lend it any.

23. Security

A journey describes work. It is not a place for authority, code or secrets, and a processor MUST reject a journey that tries to use it as one. A program's interface grants no permissions: what a user may do is decided by the platform, whatever a screen shows.

Annex A. Examples

This annex is informative. Each example is a conforming journey.

A.1 Acknowledge one result

The smallest useful journey: one client node and one terminal node.

{
  "identity": "example:acknowledgment",
  "name": "Acknowledge one result",
  "inputs": {
    "result_message": "Ready"
  },
  "entry": "acknowledge",
  "nodes": [
    {
      "code": "acknowledge",
      "type": "client",
      "instruction": {
        "goal": "acknowledge_result",
        "message": "Show the result and obtain acknowledgment."
      },
      "inputs": {
        "result_message": {
          "from": "$journey.inputs.result_message"
        }
      },
      "outputs": {
        "acknowledgment": {
          "type": "object",
          "required": [
            "confirmed"
          ],
          "properties": {
            "confirmed": {
              "const": true
            }
          },
          "additionalProperties": false
        }
      },
      "next": "completed"
    },
    {
      "code": "completed",
      "type": "server",
      "terminal": true,
      "inputs": {},
      "outputs": {}
    }
  ]
}

A.2 Wait for one event

A journey that waits for an event, with separate terminal nodes for success, expiry and failure.

{
  "identity": "example:configured-gmail-callback",
  "name": "Wait for one configured Gmail event",
  "inputs": {},
  "bootstrap": {
    "callbacks": [
      {
        "code": "mail",
        "source": "gmail.trigger",
        "inputs": {},
        "match": {
          "left": {
            "from": "$event.fields.subject"
          },
          "op": "exists"
        }
      }
    ]
  },
  "entry": "wait_for_mail",
  "nodes": [
    {
      "code": "wait_for_mail",
      "type": "server",
      "operation": "bosl.callbacks.await",
      "inputs": {
        "callback": {
          "value": "mail"
        },
        "ttl_seconds": {
          "value": 60
        }
      },
      "outputs": {
        "event": {
          "type": "object",
          "required": [
            "kind",
            "reference"
          ],
          "properties": {
            "kind": {
              "const": "callback_event"
            },
            "reference": {
              "type": "string",
              "pattern": "^bosevt_[A-Za-z0-9_-]{32}$"
            }
          },
          "additionalProperties": false
        }
      },
      "next": "completed",
      "catch": {
        "cases": [
          {
            "when": {
              "left": {
                "from": "$error.code"
              },
              "op": "eq",
              "right": {
                "value": "CALLBACK_WAIT_EXPIRED"
              }
            },
            "next": "wait_expired"
          }
        ],
        "default": "wait_failed"
      }
    },
    {
      "code": "completed",
      "type": "server",
      "terminal": true,
      "inputs": {},
      "outputs": {}
    },
    {
      "code": "wait_expired",
      "type": "server",
      "terminal": true,
      "inputs": {},
      "outputs": {}
    },
    {
      "code": "wait_failed",
      "type": "server",
      "terminal": true,
      "inputs": {},
      "outputs": {}
    }
  ]
}

A.3 Observe one result

A journey with no external effect.

{
  "identity": "example:notification-observation",
  "name": "Observe one no-send journey result",
  "inputs": {
    "result_message": "The no-send journey result was observed."
  },
  "entry": "observe_result",
  "nodes": [
    {
      "code": "observe_result",
      "type": "client",
      "instruction": {
        "goal": "observe_journey_result",
        "message": "Read this harmless journey result to verify notification delivery. No customer or provider action is required."
      },
      "inputs": {
        "result_message": {
          "from": "$journey.inputs.result_message"
        }
      },
      "outputs": {},
      "next": "completed"
    },
    {
      "code": "completed",
      "type": "server",
      "terminal": true,
      "inputs": {},
      "outputs": {}
    }
  ]
}