Language reference
Agama DSL reference#
This document surveys the different constructs of the Agama domain-specific language.
Code comments#
There is support for single line comments only (no block comments). Use //
to start a comment. Comments can start anywhere in a line.
Data types#
In practice, values would fit into any of: string, boolean, number, list or map. Since routines implemented in other languages can be invoked (more on this later), returned values might not match up exactly with these categories, however this is not too relevant because there is no strict type enforcement in Agama.
Literals#
Strings#
-
They are surrounded by double quotes. Examples:
"Agama"
,"blah"
,""
(empty string) -
Backslash can be used to escape chars, like
"Hello\nGluu"
(line feed),"Hi\u0040"
(unicode character) -
Including double quotes in strings requires unicode escaping, like
"\u0022"
. Using"\""
won't work
Booleans#
- Only
true
orfalse
allowed (notice they are lowercased)
Numbers#
-
They are expressed in base 10 only
-
Can be signed or unsigned, with or without decimal:
0
,-1
,2.0
,2.3
,-3.000001
, etc. -
No exponential notation allowed (e.g.
1E-05
) -
The following are not valid:
.1
,-.1
,+1
. These are their OK equivalents:0.1
,-0.1
,1
Null#
The “special” value null
can be used (responsibly) to represent the absence of a value. It is a direct consequence of supporting the integration of other languages in the DSL.
Lists#
-
They are finite sequences. Elements are separated by commas
-
Examples:
[ 1, 2, 3 ]
,[ "bah!", "humbug" ]
,[ ]
(empty list) -
Elements of a list do not have to be of the same type:
[ false, [ 0, 1], "?" ]
is legal but generally discouraged -
Commas can be surrounded by any combination of spaces and new lines. This is handy when a list takes up some space. This is legal:
[ "no", "such", "thing"
, "as", "a",
"stupid","question"]
Maps#
-
They are in essence associative arrays (a.k.a. dictionaries): unordered collections of key/value pairs
-
Example:
{ brand: "Ford", color: null, model: 1963, overhaulsIn: [ 1979, 1999 ] }
. This map keys arebrand
,color
,model
, andoverhaulsIn
-
In literal notation, keys names must follow the pattern
[a-zA-Z]( _ | [a-zA-Z] | [0-9] )*
so these are all valid key names:a
,Agama
,b_a
,a0_0
; on the contrary,_a
,9
,-a
, and"aha"
are invalid key names -
As with lists, commas can be surrounded by any combination of spaces and new lines
Variables#
-
Variable names follow the pattern:
[a-zA-Z]( _ | [a-zA-Z] | [0-9] )*
-
camelCase naming is recommended
-
Variables are not declared, just used freely. Variables are always global in a given flow
-
They can be assigned a value using the equal sign. Example:
colors = [ "red", "blue" ]
-
They can be assigned several times in the same flow
Accessing and mutating data in variables#
Strings#
-
Suppose
x
is a string. Individual characters can be accessed by zero-based indexes:x[0]
,x[1]
, etc. and they are themselves considered strings of size 1 -
x.length
returns the string size (number of characters in it). -
Strings are not modifiable (neither size nor individual characters can be altered)
Lists#
-
Suppose
x
is a list. Elements can be accessed by zero-based indexes:x[0]
,x[1]
, etc. -
Elements of a list can be assigned (and re-assigned) using indexes too. Example:
x[2] = false
-
x.length
returns the list size. This value can be updated in order to shrink or grow a list (e.g.x.length = 10
). When extending a list beyond its current length, the “gap” created is filled withnull
values -
An attempt to access an index position greater than or equal to the list size returns
null
(most general-purpose languages would raise a runtime error in this situation) -
Using expressions for indexing is not allowed, like
x[person.age]
,x[y[0]]
-
Click here to learn more about access in lists
Maps#
-
Suppose
x
is map. Values can be accessed by using “dot notation” -
Say
x = { brand: "Ford", color: null, model: 1963, overhaulsIn: [ 1979, 1999 ] }
, thenx.model
evaluates to1963
, andx.overhaulsIn[1]
evaluates to1999
-
Setting the color would be like
x.color = "white"
-
A new key/value pair can be appended too:
x.maxSpeed = 90
-
Access of an unknown property evaluates to
null
:x.owner
-
If a key name does not follow the pattern
[a-zA-Z]( _ | [a-zA-Z] | [0-9] )*
an alternative notation must be employed to retrieve or modify the associated value. Click here to learn more
Flow structure#
A flow written in Agama consists of a header and one or more statements following.
Header basics#
A header is declarative by nature. It starts with the Flow
keyword followed by the qualified name of the flow, e.g. Flow com.acme.FoodSurvey
. A qualified name is a sequence of one or more fragments separated by dot (.
) where a fragment follows the pattern [a-zA-Z]( _ | [a-zA-Z] | [0-9] )*
. By convention, qualified names shall adhere to a reverse Internet domain name notation.
In a header, at minimum, a base path literal string must be provided (in an indented block). This sets the location where flow assets will reside relative to the assets root:
Flow com.acme.FoodSurvey
Basepath "mydir"
Here, assets refer to all elements required to build the end-user experience: UI pages, stylesheets, images, etc. The storage of these elements and location of the "root" are specific to the concrete Agama engine implementation used. Generally it will resemble a directory structure in a filesystem.
Next, flow timeout may be specified. This is the maximum amount of time the end-user can take to fully complete a flow. For instance:
Flow com.acme.FoodSurvey
Basepath "mydir"
Timeout 100 seconds
An unsigned integer literal value must be specified after the Timeout
keyword, if present.
The Configs
keyword may be used to designate a variable so the flow's properties can be accessed in the code. These properties are usually provided when the flow is created - normally through an administrative tool. This process varies from engine to engine.
Often flow properties are used to supply configuration parameters to the flow. As an example, suppose a flow that sends a confirmation e-mail has to be implemented. Properties are a good place to hold the configuration of the outgoing mail server to employ: name, port, authentication credentials, etc. For instance, this is how the configuration properties would be bound to variable conf
:
Flow com.acme.FoodSurvey
Basepath "mydir"
Timeout 100 seconds
Configs conf
Inputs#
In addition to the above, and optionally too, flows may receive inputs from their callers. Input names can be listed using the Inputs
keyword:
Flow com.acme.FoodSurvey
Basepath "mydir"
Inputs salutation askGender promptRealName
Input names follow the same naming conventions (patterns) of variables and can be treated as such in code.
Important
Note the difference between properties and inputs. Properties are parameters that callers of the flow should not control or be interested in. On the other hand, inputs are parameters that callers supply explicitly to make the flow exhibit certain behaviors.
Check this section to learn how callers can pass values to input parameters of a flow.
Flow statements#
The statements that make up a flow - body - come after the header and start at column 1, ie. aligned with the Flow
keyword:
Flow com.acme.FoodSurvey
Basepath "mydir"
Timeout 100 seconds
Configs conf
Inputs salutation askGender promptRealName
x = "Hi"
y = false
...
There are several types of statements: branching, looping, web interaction, etc. They will be regarded in the subsequent sections of this document. Note Agama does not support the concept of subroutines/procedures; this is not needed because functional decomposition is carried out by calling subflows.
Logging#
Flows can issue small messages (normally used as a form of troubleshooting) that will be appended to a log. Every message can be associated a severity level. Both the log location and available levels are engine specific.
To append data to the flows log, use the Log
instruction. Examples:
Code | Message appended | Notes |
---|---|---|
Log "Hi there" |
Hi there | |
Log "Hello" "world" |
Hello world | Log can be passed a variable number of parameters |
Log "Hello" "world" 0 false |
Hello world 0 false | |
Log [1, 2, 3, 4, 5] |
1, 2, 3, ...more | Lists and maps are not traversed wholly |
Log "Hell%% 0 %" "o" " world" false |
Hello world 0 false | Placeholders usage |
Log "% % % yes" 1 "two" |
1 two % yes | |
Log "3" "%" 0 |
3 % 0 | |
Log "@warn Today is Friday %th" 13 |
Today is Friday 13th | Message logged as warning (if the engine features a "warning" level) |
Log "@w Today's Armageddon \u263A" |
Today's Armageddon ☺ | Message logged as warning (if the engine features a "w" level - shortcut method) |
Check your engine's documentation to learn more about how statements are logged.
Conditionals and branching#
Keywords When
and Otherwise
allow to write conditionals. With and
, or
, is
, and is not
, logical (boolean) expressions can be built. Examples:
car = { brand: "Ford", color: null, model: 1963 }
When car.color is null
car.color = "pink"
...
Nested conditionals:
...
When car.color is "pink"
When car.brand is "Ford"
Log "Weird-looking car"
...
Use of Otherwise
:
...
When car.color is not null
Log "you have a regular painted car"
...
Otherwise
...
Boolean expressions can span several lines: and
and or
can be at the start or end of a line as long as the whole expression is left-aligned with its corresponding When
. Examples:
//legal:
When day is cloudy
When there is hope and
there is mercy
Log "let's rock n' roll"
...
When day is cloudy
When there is hope and
// :D
there is mercy
or fear is null
Log "let's rock n' roll"
...
//illegal:
When day is cloudy
When there is hope and
// :D
there is mercy
or fear is null
Log "let's rock n' roll"
...
When day is cloudy
When there is hope and
// :D
there is mercy
or fear is null
Log "let's rock n' roll"
...
Notes:
-
Equality is designed to work with
null
, numbers, strings, and boolean values only. More exactly, a number should only be compared to a number, a string to a string, etc., otherwise the equality test evaluates tofalse
. Comparing a value with itself evaluates to true regardless of type, i.e.car is car
,null is null
,false is false
are all truthy -
Comparisons are limited to equality (
is
) or inequality (is not
). For other forms of comparison you can resort foreign routines -
As expected
and
has higher priority thanor
when evaluating expressions. There is no way to group expressions to override the precedence: there are no parenthesis in Agama. Assigning the result of a boolean expression to a variable is not supported. These restrictions are important when writing conditionals
Advanced matching#
Agama's Match ... to
is a construct similar to C/Java switch
. Example:
car = ...
x = ...
y = ...
// Assume x and y hold numbers
z = [ 3.1416, 2.71828 ]
Match car.model to
x
//Code in this block will be executed if car.model is equal to the value of x
...
-y //Here we use minus y
...
z[0]
...
1.618 //Literal values can be used too for matching
...
null
...
Otherwise //optional block
//Instructions here are executed if there was no match at all
Flow finish#
Finish
is used to terminate a flow's execution. A flow can finish successfully or failed. Examples:
Code | Meaning |
---|---|
|
Shorthand for flow finished successfully |
|
Shorthand for failed flow |
|
Flow finished successfully. Some relevant data attached |
|
Flow failed. Error description attached |
|
Shorthand for { success: true, data: { userId: "as9233Qz" } } |
|
This causes the flow to crash. Note this is not equivalent to Finish false (which means the flow ended with a negative outcome). |
Notes:
- Unless otherwise stated by the concrete engine implementation, a map literal should not be passed directly as argument. This means the following is illegal:
Finish { success: false, error: "spacetime singularity" }
. The examples above list several syntactically valid usages - Any statement found after
Finish
is not reached and thus, not executed - If no
Finish
statement is found in a flow's execution, this will degenerate in flow crash - When a flow is finished and was used as subflow (part of the execution of a bigger parent flow), the parent does not terminate. Execution continues at the following instruction that triggered the subflow. More on
Trigger
later - Using
data
in theFinish
directive is an effective way to communicate information to callers (parent flows) - Learn more about flows lifecycle here
Web interaction#
Web interaction constructs bring the most value to Agama. Developers can express the concepts of ”redirect a user to an external site and retrieve any data later provided” or “show a page and grab user data after interaction” using atomic instructions.
RFAC#
RFAC
(stands for Redirect and Fetch at callback) abstracts the process of redirecting the user's browser to an external site and then collect the data presented later at a designated callback URL. This feature is particularly useful in inbound identity scenarios (e.g. to support social login).
Example | Details |
---|---|
|
Redirects to the given location. Once the user browser is taken to the callback URL by the external site (twitter.com), the flow continues ignoring any data included |
|
Redirects to the given location. Once the user browser is taken to the callback URL by the external site, the data included in the query string or payload is stored in result (a map) for further processing |
The callback URL varies depending on the engine used. Check your engine's docs.
RRF#
RRF
(stands for Render-Reply-Fetch) abstracts the process of rendering a UI template, send the produced markup to the browser and grab user-provided data back at the server side.
Example | Details |
---|---|
|
Renders the template survey.htm (located in this flow's base path) and resulting markup is replied to user's browser.Data submitted by the user is ignored |
|
Renders the template survey.htm by injecting the data passed in obj and the resulting markup is replied to user's browser.Data submitted by the user is stored in variable result : a map whose keys are named according to the form fields present in survey.htm
|
Notes:
- The template location must be specified with a string literal only (not a variable)
- Where and how to store templates is an engine-specific detail as well as the file formats supported. See Assets management.
- Use map variables - not literal maps - for the second argument of
RRF
Looping#
There are two constructs available for looping in Agama: Repeat
and Iterate over
.
Repeat#
Repeat
was designed with the concept of attempts/retries in mind: a set of statements are executed, a condition can optionally be supplied in order to abort the loop early, and (optionally too) a block of statements can be executed before the next iteration is started if the condition evaluated to false
. A loop is given a maximum number of iterations. Examples:
Example | Notes |
---|---|
|
A loop that runs 3 iterations at most. A page is shown at every iteration. If the value entered by the user matches that of month variable, the loop is aborted earlier |
|
Similar to previous example This time the max no. of iterations is set using a variable When there is a miss a message error is set (which the UI template may potentially use) |
|
Similar to previous example After the loop finishes, variable y will contain the total number of iterations made to completion. This excludes partial iterations aborted by Quit , thus, y <= x Note the usage of implicit variable idx which holds the current (zero-based) iteration number |
Iterate over#
Iterate over
is used to traverse the items of a string, list, or the keys of a map. At every iteration, a variable is set with the current item or key name. As with Repeat
, a loop may be aborted earlier, an optional block of statements can be specified after Quit
, and the total number of iterations can be stored in a variable.
Example | Notes |
---|---|
|
A loop running over a simple list. Every element visited is referenced with variable sn |
|
Iterates over the keys of the map printing both the key and its associated value. To learn about the .$ notation see Maps and dot notation |
|
Nested loops |
|
Similar to the previous example. The inner loop is aborted upon a given condition. Note the total number of complete iterations is recorded in y every time the inner loop finishes. |
|
Prints iteration numbers: 0 0, 0 1, 0 2, 1 0, 1 1, 1 2, ... 3 2 The index used in idx is 0 for the outermost loop and increases by one at every level of loop nesting |
Subflows#
A flow can Trigger
another flow (a.k.a subflow) and grab its response when Finish
ed. This feature materializes flow composition and re-use in Agama.
Example | Notes |
---|---|
|
Starts the flow with qualified name jo.jo.PersonalInfoGathering .Returned data is ignored |
|
Starts a flow passing parameters (assuming PersonalInfoGathering receives two inputs).outcome will contain the map used when the subflow ended |
|
Similar to the previous example. If for some reason PersonalInfoGathering crashes, variable E will hold a reference to the error for further processing. Otherwise E evaluates to null . The type/structure of E is an engine-specific detail. The variable on the left of the pipe (| ) can be omitted if the outcome of the flow will not be inspected |
|
Starts a flow whose qualified name is determined at runtime |
Input parameters#
The values passed after the flow name in Trigger
are supplied as input parameters in the order declared by the subflow's Inputs
. When not enough values are passed, the unassigned inputs will hold a null
value.
Unless otherwise stated by the concrete engine implementation, list and map literals should not be passed as arguments to Trigger
:
-
Illegal:
Trigger subflow { key: [ 1, 2 , 3] } [ "Yeeha!" ]
-
Legal:
Trigger subflow x car.model list[1] null false -3 "Sam"
Template overrides#
When re-using flows, existing templates may not match the required look-and-feel or layout of the flow that is being built, or may require minor adjustments to fit better the parent's needs. These can be overcome by declaring which templates the developer would like to override for a given subflow call. Example:
outcome = Trigger jo.jo.PersonalInfoGathering
Override templates "path/basic.htm" "" "path2/dir/detail.htm" "tmp/mydetail.htm"
Log "subflow returned with success?" outcome.success
In an indented block using the Override templates
keyword, several string literals can be provided. They specify the paths of the (subflows) templates that will be overriden by the parent and the corresponding new paths. In the example above, templates path/basic.htm
and path2/dir/detail.htm
rendered by flow PersonalInfoGathering
(or its subflows) won't be picked from these locations but from the base path of the current (parent) flow using names basic.htm
and tmp/mydetail.htm
respectively. Usage of empty string denotes reusing the same file name than the original file.
Alternatively, every pair of original vs. new path can be specified in a single line for more clarity, like this:
outcome = Trigger jo.jo.PersonalInfoGathering
Override templates "path/basic.htm" ""
"path2/dir/detail.htm" "tmp/mydetail.htm"
...
Note
The new (overriding) templates will be injected with the same data original templates would receive. In other words, this directive only "modifies" the first parameter of RRF
instructions.
Foreign routines#
Business logic implemented in languages other than Agama can be re-used by means of the Call
instruction. Call
plays a key role because Agama code serves fundamentally as a depiction of a flow hiding most of the internal details and low-level computations which are in turn delegated to foreign routines.
The syntax of this directive is very general and actual semantics are left to the concrete engine. Roughly, here is how a Call
can be structured:
( var_expr? ('|' var_name)? = )? Call alnum(.alnum)* (#alnum)? arg*
where:
- var_name is a variable name, e.g.:
x
,fooBar
,x_0
- var_expr is a variable expression, e.g.:
x
,x.a
,x[1].b
, etc. - alnum is syntactically identical to var_name but does not necessarily refer to an existing variable in the code, for instance, it may describe a path to locate a routine in a library
- arg is a var_expr or any string, number, boolean, null literal
So the below are all syntactically valid invocations:
Call apple#pear
Call apple#pear "Hi" x 16
Call apple.banana#pear "Hi" abc 16
foo = Call apple.banana#pear "Hi" abc 16
foo | error = Call apple.banana#pear "Hi" abc 16
| error = Call apple.banana#pear "Hi" abc 16 x[0].ahoy.ahoy
foo = Call street fighters true false
Again, the semantics are given by the specific engine. Consult your engine documentation for examples.
Unless otherwise stated by the concrete engine implementation, list and map literals should not be passed as arguments to method calls directly. This means the following is illegal: Call co.Utils#routine { key: [ 1, 2 , 3] } [ "Yeeha!" ]
.
Advanced and special cases in variable manipulation#
Indexing in lists#
Accessing/modifying list elements requires providing a numeric index between the brackets, e.g. x[ 0 ]
. Note variables can also be used for indexing, like x[ y ]
where y
is a positive integer or zero.
For the below table, assume x = [ "one", "two", "three" ]
.
|
|
|
|
||
|
Maps and dot notation#
The regular “dot” notation is limited in the sense it is fairly static: developers have to have prior knowledge about the keys' names, in other words, about the structure of maps and nested submaps, like in person.homeAddress.postalCode
.
Also, there might be cases where a key name does not fit the required pattern, like in person.street-address
or persona.dirección
; even worse, there might be cases where the actual key is only known at runtime.
There are ways to overcome this:
Example | Notes |
---|---|
|
Access the value associated to the key named -wow! |
|
Access the value associated to the key whose name is contained in the variable prop (that holds a string value). Note actual value of prop may be originated from a Java call or another form of computation |
|
A mix of notations is valid. For example, if x= { a: { b: 0, c: { c: true, d: { e: null, f: "hello" } } } } , propA is equal to "a" , and propB to "f" , the expression on the left evaluates "hello" |
Usage of .$
requires to supply a variable after the dollar sign: grouped variable expressions are not supported. Thus, it is not possible to achieve something like x.a.c.($map.mykey).f
in order to obtain "hello"
if map = { mykey: "d" }
.
Indexing in maps#
For convenience, when a key name “looks like” a positive integer (or zero), e.g. "2"
, "10"
, etc., numeric values can directly be used to get/set data in a map:
x = { }
x."1" = "golf"
x[1] //retrieves "golf"
x[2] = "polo" //adds the key/value pair "2" / "polo"
Language keywords#
The following is a list of reserved words and as such, cannot be used as variable names or maps keys (in literal notation).
Keyword | Purpose/usage |
---|---|
Basepath | header declaration |
Call | Java interaction |
Configs | header declaration |
Finish | termination |
Flow | header declaration |
Inputs | header declaration |
Iterate over | loops |
Log | logging |
Match | conditionals |
Otherwise | conditionals |
Override templates | web interaction |
Quit | conditionals and loops |
Repeat | loops |
RFAC | web interaction |
RRF | web interaction |
seconds | header declaration |
times max | loops |
to | conditionals |
Timeout | header declaration |
Trigger | subflow calls |
using | loops |
When | conditionals |
Operator |
---|
and |
is |
is not |
or |
Special literals |
---|
true |
false |
null |
Created: 2022-06-24