Program structure

Context

Conversation context is a set of variables, that you may use during the conversation. You can define input, output and additional context variables.

context { // Input context variables input input_var1: number = 0; input input_var2: string | null; // Additional context variables context_var1: number = 0; context_var2: string | null // Output context variables output output_var1: number = 0; output output_var2: string | null; }

Node

A node describes the system's actions in the current state and the conditions for transitioning to other nodes.

Every node has its own <name>, stated after the node keyword.

Structure

Every node contains the following sections:

  • do - a section describing actions performed in this node;
  • transitions - a section describing transitions to subsequent nodes;
  • onexit - an optional section describing the actions performed when exiting the node through certain transitions;
  • ondigreturn - an optional section describing the actions to be performed when returning to the current node from a digression.

Example

node foo { do { /// a section describing actions performed in this node } transitions { /// a section describing transitions to subsequent nodes } onexit { /// an optional section describing the actions performed when exiting this node through specific transitions } }

Node's local data

Local data can only be accessed within the scope in which it is declared. Like context data, local data is statically typed.

Local data is not shared between executable sections of the node. For example, you cannot access the data declared in the do section from the onexit section of the same node.

Lifetime

At runtime, when entering an executable section (for example, do section), an isolated scope containing local data is created. When the section's execution ends, the scope will be canceled, and its local data will be destroyed.

Example

start node myNode { do { // Declare local variable. var foo: number = 0; var bar = "Hello"; // Read from local variable. // Expected output: "Hello" #log(bar); } transitions { } }

Dialogue entry point

Every dialogue must contain a node marked with the start keyword. This node will be an entry point of your dialogue, just like the main function in other programming languages.

start node entry_point { do { } }

Transitions

Transitions describe conditions of choosing the next node. transitions section of a node is just an enumeration of named transitions.

start node myNode { do { // Triggers waiting mode. wait *; } transitions { // Declaration of transition "transitionName" to node "nodeName" triggered by condition "myCondition". transitionName: goto nodeName on myCondition; } }
  • transitionName specifies the name of the transition.
  • nodeName specifies target node.
  • myCondition specifies transition's condition.

You can omit the transitions section entirely in nodes with no transitions (e. g., terminating nodes).

Transition on event

The most common way to trigger transitions is by waiting for an event. To decide which transition to trigger, an application checks the condition of every active transition in descending order of priorities until one of them is true. You can activate transitions with wait operator, flags and transition tags.

Here’s an example illustrating all the possible ways of declaring a transition on an event. All the described fields are optional. More often than not, you won’t need to use all of them in one transition:

node myNode { do { wait *; } transitions { tr: goto targetNode on #messageHasSentiment("positive") priority 100 tags: ontext; } }
  • on #messageHasSentiment("positive") is a declaration of a condition (in this case transition is triggered with a positive sentiment);
  • priority (0 by default) allows to control the order of condition checks while handling an event: conditions are checked in descending priority order;
  • tags: ontext declares types of events triggering the transition (by default, tag ontext is used).

Timer transition

A timer transition can be declared using the on timeout <anyNumber> keyword, where anyNumber specifies the timeout in milliseconds. The timer starts when the transition is activated by the wait statement.

node myNode { do { wait *; } transitions { timeoutTransition: goto targetNode on timeout 5; } }

Instant transition

An instant transition does not contain a condition description and is ignored when checking for events. Such transitions can be triggered with the goto statement placed in the do section (not to be confused with the goto keyword in the transition declaration).

node myNode { do { goto instantTransition; } transitions { instantTransition: goto targetNode; } }

Library

The library is a plug-in code. During compilation, its content is copied to the place where the library was imported.

Library file description

A file is declared library using the library directive at the beginning of the file. To include the library's content, use the import keyword followed by the path to the linked library.

An example of declaring and importing a library file:

Library file myLib.dsl

library // A directive that declares the file to be a library. block myLibBlock(): boolean { start node root { do { #say("Hello from lib block!"); return true; } transitions { } } }

Main file

import "./myLib.dsl"; // Connecting the library file "myLib.dsl". start node hello { do { #connect("myEndpoint"); set $x = blockcall myLibBlock(); // Calling a block from a library file. exit; } transitions { } }

Block

The block is a graph analog of subroutines that provides a mechanism for code reuse. The main graph is implicitly a block too.

Block description

A block is declared using the block keyword. A block can contain nodes, digressions and nested blocks.

block MyBlock(): void { start node root { do { // Custom code... } transitions { } } }

Returning from a block

To terminate the block and continue the graph's execution from the point the block was called at, use the return keyword. You may also return some value.

Block call

Block call is performed with the blockcall keyword, followed by the block's name and its parameters.

Example

start node foo { do { set x = blockcall bar("in", y: "put"); set $out = x; exit; } transitions { } } block bar(x: string, y: string): string { start node baz { do { return $x + $y; } transitions { } } }

Block context

Context is the isolated scope of the block. Block context is similar to main conversation context. The difference is that block context is isolated and main conversation context is not available inside the block. If you need to assign something to main conversation context return these values from block. To define block context use the context directive inside the block.

The context can be accessed from any executable section and transitions inside the block using the $ prefix.

Example

start node root { do { blockcall MyBlock("Hello", "from"); exit; } transitions { } } // Declaration of block "MyBlock" with context `{ arg1: string, arg2: string }`. block MyBlock(arg1: string, arg2: string): boolean { // Context extension directive. context { // Add "myInputData" to context. myInputData: string = "MyBlock"; } start node root { do { // Expected output: "Hello" #log($arg1); // Expected output: "from" #log($arg2); // Expected output: "MyBlock" #log($myInputData); return true; } transitions { } } }

Async Block

Modificator async denotes that this block is asynchronous. Asynchronous block is the block which is executed asynchronously, i.e. in non-blocking way. Execution of an asynchronous block can be thought as starting another dialogue at the same time (like an async thread).

Asynchronous block allow you to call #connect() and #connectSafe function (only once), i.e. to establish new single connection during the dialogue.

Only asynchronous blocks are allowed to call other asynchronous blocks.

Main graph context is technically an asynchronous block.

When created, the id assigned to it. This id can be used to route builtin functions controlling the behavior of channels established in async blocks.

When terminated, async block sends messages of type "Terminated" to all other blocks (see Async Block Messages).

async block MyAsyncBlock(...args): returnType {}

Builtin functions to control async blocks behaviour:

Async Block Messages

To communicate with each other there is a mechanism of messages between async blocks.

type AsyncBlockMessage = { // notifies about bridging and undridging of async block channels biDirectional:boolean; messageType:"Unbridge"|"Bridge"; sourceRouteId:string; targetRouteId:string; }|{ // notifies that another async block terminating // (or is being terminated by) the current block messageType:"Terminate"|"Terminated"; sourceRouteId:string; targetRouteId:string; }|{ // notifies about sending content messages between async blocks content:unknown; messageType:"Content"; sourceRouteId:string; targetRouteId:string; }?

Digression

Digressions are a special type of nodes accessible from any other node in the graph despite not having any explicit transitions in them. Digressions allow a succinct description of reactions to context-independent events (e. g., questions like "what's your name") or events that may occur in several nodes.

After reacting to a digression, the graph may either proceed to the next node via ordinary transitions or return to the previous node with the return keyword.

Examples

start node root { do { wait *; } transitions { } } // digression declaration digression myDigression { // digression's conditions conditions { on myCondition; } do { // return from the digression return; } transitions { } }
digression myDigression { condition on myCondition do { goto digressionContinuation; } transitions { digressionContinuation: goto digressionContinuation; } } node digressionContinuation { do {} transitions {} }

Preprocessor

A preprocessor is a special kind of digression that doesn't consume the message. Upon preprocessor's return, the wait operator will continue processing transitions. Preprocessor digression can't have any transitions from it.

Preprocessor digressions are declared with the preprocessor modifier.

Example:

// preprocessor declaration preprocessor digression myPreprocessor { conditions { on myCondition; } do { // return to the waiting node and continue event handling return; } }

Digression control

Digressions within a block are enabled by default, i.e. their conditions are checked when handling an event. digression disable { <digressionList> } operator allows disabling digressions at runtime. Disabled digressions are ignored when handling an event. Digression could be enabled back via digression enable { <digressionList> } operator.

You can use the digression save { <digressionList> } operator to save states of several digressions in a variable of a special type — digression mask. It is handy when you need to alter the state of many digressions temporarily. Use the digression apply <digressionMask> operator to restore the digressions' state from a saved mask.

You may omit curly braces when switching or saving the state of a single digression (e. g. digression disable whats_your_name).

Examples

/** * insert declaration of "foo" and "bar" digressions */ start node myNode { do { // All digressions is enabled by default so "initialDigressionMask" // get mask with "foo" enabled and "bar" enabled. var initialDigressionMask = digression save { foo, bar }; digression disable { foo, bar }; // Now both digressions are disabled. digression enable foo; // Now "foo" is enabled and "bar" is disabled. digression apply initialDigressionMask; // Now both digressions are enabled. exit; } transitions { } }

Digressions activity status inheritance

If a child block contains a shared digression, its activity status will be inherited. However, if you change the shared digression's status in the child block, it won't be changed in the parent one.

Examples

File sharedDigression.dsl

library digression sharedDigression { conditions { on true; } do { #log("Hello from foo"); return; } transitions {} }

File main.dsl

import "sharedDigression.dsl"; start node root { do { digression disable sharedDigression; // Now digression "sharedDigression" is disabled. blockcall myBlock(); // Digression "sharedDigression" still disabled. exit; } transitions {} } block myBlock(): boolean { import "sharedDigression.dsl"; start node root { do { // Digression "sharedDigression" is disabled because of mask inheritance. digression enable sharedDigression; // Now digression "sharedDigression" is enabled. return true; } transitions {} } }

Digression parameters

Digressions can have some parameters to be read and changed from outside the digression itself. It may be useful for making reusable libraries.

Digression parameters are declared between the conditions and do section with the var keyword:

digression myDigression { conditions { on myCondition; } var myDigressionData: string = "defaultValue"; do {} transitions {} }

Shared digressions inherit parameter values. Keep in mind that parameters changed in the child block don't do so in the parent one unless declared with the shared modifier.

Accessing digression parameters

Digression's parameters are accessible from any executable section and condition in the block. You can use them in the same way you use variables. Use the format digression.digression_name.parameter_name to access the parameters:

/** * insert "targetNode" declaration here */ node myNode { do { // expected output: "defaultValue" #log(digression.myDigression.myDigressionData); // modify digression parameter set digression.myDigression.myDigressionData = "customValue"; // expected output: "customValue" #log(digression.myDigression.myDigressionData); wait *; } transitions {} }

Digression parameter inheritance

Different blocks may contain digressions of the same name; such digressions are considered the same. If the block contains an instance of the same digression as its parent (or any other predecessor), this digression inherits its parent's parameters' values. Such digressions are called shared.

Example of parameter value inheritance

File sharedDigression.dsl

library // This digression will be imported in the root block and in "myBlock", // so it's considered shared between them. digression sharedDigression { conditions { on #messageHasSentiment("positive"); } // Initialize value in the scope of the root block. var data: string = "defaultValue"; do { return; } transitions {} }

File main.dsl

import "sharedDigression.dsl"; start node root { do { // Expected output: "defaultValue" #log(digression.sharedDigression.data); set digression.sharedDigression.data = "newValue"; // Block "myBlock" will inherit `digression.sharedDigression.data` value. set tmp = blockcall myBlock(); exit; } transitions {} } block myBlock(): boolean { // Because an instance of "sharedDigression" exist in the calling block, // "myBlock" will inherit its data. import "sharedDigression.dsl"; start node root { do { // Expected output: "newValue" #log(digression.sharedDigression.data); return true; } transitions {} } }

Parameters are copied on inheritance. So, changing the parameter's value in the child block does not affect its value in the parent block:

The peculiarity of copy inheritance of parameter values

File sharedDigression.dsl

library // This digression will be imported to root block and to "myBlock", // so it considered shared between them. digression sharedDigression { conditions { on #messageHasSentiment("positive"); } // Initialize value in the scope of the root block. var data: string = "defaultValue"; do { return; } transitions {} }

Файл main.dsl

import "sharedDigression.dsl"; start node root { do { // Expected output: "defaultValue" #log(digression.sharedDigression.data); blockcall myBlock(); // Expected output: "defaultValue" #log(digression.sharedDigression.data); exit; } transitions {} } block myBlock(): boolean { import "sharedDigression.dsl"; start node root { do { // Modify shared digression parameter in child block. set digression.sharedDigression.data = "newValue"; // Expected output: "newValue" #log(digression.sharedDigression.data); return true; } transitions {} } }

Shared parameters

Sometimes it's necessary to share a parameter's value between several digression instances. You can do this by declaring the parameter as shared.

An example of declaring and using a shared parameter

File sharedParameter.dsl

library // This digression will be imported to root block and to "myBlock", // so it considered shared between them. digression sharedDigression { conditions { on #messageHasSentiment("positive"); } // Initialize value in the scope of the root block. shared var data: string = "defaultValue"; do { return; } transitions {} }

File main.dsl

import "sharedParameter.dsl"; start node root { do { // Expected output: "defaultValue" #log(digression.sharedDigression.data); blockcall myBlock(); // Expected output: "newValue" #log(digression.sharedDigression.data); wait *; } transitions {} } block myBlock(): boolean { import "sharedParameter.dsl"; start node root { do { // Modify shared digression parameter in child block. set digression.sharedDigression.data = "newValue"; // Expected output: "newValue" #log(digression.sharedDigression.data); return true; } transitions {} } }

Immediate reaction

Using when constructions, you can handle immediate reaction on converstation event.

when <event> [yourName] [priority number] do { ...some code };

Where event is required and can have following values:

  • starting - executed before start node
  • exiting - executed just after exit instruction (or return inside block)
  • opened - executed after a successful call of #connect or a #connectSafe call
  • blockMessage - executed when any async block sends a message to this one

Each reaction will be executed in an order sorted by descending priority (Firstly with priority N, then with priority N-1 etc.)
Each when section can call an exit, except when exiting.

Exit reaction

You can write multiple when exiting do { ...some code } for handling end of the conversation and do some post-processing,(e.g. analyze a conversation or call your backend).

If an exit occurs in the block, then all exiting whens will be called in the current block, and then in the parent block, until stack will unwind to the empty one.

Block return someValue; will call all exiting whens of this block (and only this block).

Opened reaction

You can easily limit the duration of the dialog just adding following lines

when opened do { set digression.duration_limiter.conversationStartTime = #unixTimeStamp(); } preprocessor digression duration_limiter { conditions { on #unixTimeStamp() > digression.duration_limiter.conversationStartTime + digression.duration_limiter.maxDurationSeconds*1000 tags: ontick; } var maxDurationSeconds: number = 60*60; var conversationStartTime: number = 0; do { #log("Max conversation duration exceed"); exit; } }

Handling messages from the block

Found a mistake? Email us, and we'll send you a free t-shirt!

Enroll in beta

Request invite to our private Beta program for developers to join the waitlist. No spam, we promise.