doc/ci/functions/moa.md
Moa is an expression language for dynamically constructing values during job execution.
Expressions are enclosed in ${{ }} delimiters and are used in GitLab Functions and Job inputs.
Moa supports string manipulation, arithmetic, comparisons, logical operations, property access, and function calls.
GitLab has three expression syntaxes that serve different purposes at different stages of the pipeline lifecycle.
rules: keywords
to control job inclusion. They are evaluated during pipeline creation and support
comparisons and pattern matching against CI/CD variables, but cannot perform arithmetic
or access runtime state.$[[ ]] syntax and are evaluated during pipeline creation,
before any jobs run. These expressions perform value substitution for
CI/CD inputs, matrix values, and
component inputs. They cannot perform
arithmetic, comparisons, or logic, and have no access to runtime state.
For more information, see CI/CD expressions.${{ }} syntax and is evaluated during job execution
by the runner. Moa is a full expression language with operators, data structures,
and function calls.All three syntaxes can coexist in the same pipeline. A CI/CD component that contains GitLab Functions might use all three:
spec:
inputs:
echo_version:
type: string
---
hi-job:
# rules expression - evaluated when the pipeline is created
rules:
- if: $CI_COMMIT_BRANCH == "main"
run:
- name: say_hi
# $[[ ]] - resolved when the pipeline is created
step: registry.gitlab.com/gitlab-org/ci-cd/runner-tools/gitlab-functions-examples/echo@$[[ inputs.echo_version ]]
inputs:
# ${{ }} - resolved when the job runs
message: "Hello, ${{ vars.CI_PROJECT_NAME }}"
Moa exists as a separate language because GitLab Functions need capabilities that are unavailable at pipeline creation time:
${{ steps.build.outputs.image_ref }} can be evaluated only during execution.major_version + 1), comparisons
(vulnerabilities == 0), and short-circuit logic (inputs.tag || "latest") to
construct step inputs from variables and outputs.The values available in expressions depend on where the expression is used.
| Context | Available in | Type | Evaluated | Description |
|---|---|---|---|---|
job.inputs | Job configuration: script, before_script, after_script, artifacts, cache, image, services | Object | When the Runner receives the job | Input values defined for the job. Access individual variables with job.inputs.<name>. |
env | GitLab Functions | Object | Before the function runs | Environment variables available to the function. Access individual variables with env.<name>. |
inputs | GitLab Functions | Object | Before the function runs | Input values passed to the function. Access individual inputs with inputs.<name>. |
vars | GitLab Functions | Object | Before the function runs | Job variables passed from the CI job. Access individual variables with vars.<name>. |
steps | GitLab Functions | Object | Before the function runs | Results from previously executed steps in the current function. Access a step's outputs with steps.<step_name>.outputs.<output_name>. |
export_file | GitLab Functions | String | Before the function runs | Path to the file where the function can write environment variables to export to subsequent steps. |
output_file | GitLab Functions | String | Before the function runs | Path to the file where the function writes its output values. |
func_dir | GitLab Functions | String | Before the function runs | Path to the directory containing the function's definition file. Use to reference files bundled with the function. |
work_dir | GitLab Functions | String | Before the function runs | Path to the working directory for the current execution. |
Wrap expressions in ${{ }} to evaluate them:
script:
- echo "Hello, ${{ job.inputs.name }}"
When text surrounds the expression, the result is always converted to a string. Multiple expressions can appear in a single value:
script:
- echo "${{ job.inputs.greeting }}, ${{ job.inputs.name }}!"
When ${{ expression }} is the entire value with no surrounding text, the expression
returns its native type. Use native type expressions to pass non-string values like numbers,
booleans, arrays, and objects between steps without converting them to strings.
inputs:
count: ${{ steps.previous.outputs.total }}
In this example, if total is a number, count receives a number, not the string
representation.
To include a literal ${{ in your text without triggering interpolation, escape it
with a backslash:
script:
- echo "Use \${{ to start an expression"
This command outputs the text Use ${{ to start an expression without evaluation.
The keyword null represents the absence of a value.
${{ null }}
The keywords true and false represent boolean values.
${{ true }}
${{ false }}
Numbers are IEEE 754 double-precision floating point values with 53 bits of significand precision. Integers, decimals, and scientific notation are supported.
${{ 42 }}
${{ 3.14 }}
${{ 1.5e3 }}
${{ 2E-4 }}
Enclose strings in double quotes or single quotes. The two quote types handle escape sequences and template expressions differently.
Double-quoted strings support template expressions and a full set of escape sequences:
| Sequence | Meaning |
|---|---|
\\ | Backslash |
\" | Double quote |
\n | Newline |
\r | Carriage return |
\t | Tab |
\a | Alert (bell) |
\b | Backspace |
\f | Form feed |
\v | Vertical tab |
\/ | Forward slash |
\uXXXX | Unicode code point |
\${{ | Literal ${{ (prevents interpolation) |
Template expressions (${{ }}) inside double-quoted strings are evaluated and
interpolated into the string.
Single-quoted strings are raw string literals with minimal interpretation. Template expressions inside single-quoted strings are not evaluated. Only two escape sequences are supported:
| Sequence | Meaning |
|---|---|
\\ | Backslash |
\' | Single quote |
${{ "Hello\nWorld" }}
${{ 'It\'s a string' }}
${{ 'Literal ${{ not evaluated }}' }}
Identifiers reference values from the expression context. An identifier starts with a
letter or underscore and can contain letters, digits, and underscores. Identifiers are
case-sensitive: foo, Foo, and FOO are three different identifiers.
${{ env }}
${{ my_variable }}
Identifiers are resolved against the available context. For the values available in each context, see context reference.
When an identifier refers to a context object, the entire object is returned. For example, ${{ vars }}
returns all job variables as an object.
Arithmetic operators work on numbers. The + operator also concatenates strings.
Operators do not perform implicit type conversion, so "hello" + 42 results in an error.
| Operator | Description | Example | Result |
|---|---|---|---|
+ | Addition | ${{ 2 + 3 }} | 5 |
+ | Concatenation | ${{ "a" + "b" }} | "ab" |
- | Subtraction | ${{ 10 - 4 }} | 6 |
* | Multiplication | ${{ 3 * 4 }} | 12 |
/ | Division | ${{ 10 / 3 }} | 3.333... |
% | Modulo (truncated division) | ${{ 10 % 3 }} | 1 |
Division by zero results in an error.
Comparison operators return a boolean value.
| Operator | Description | Example | Result |
|---|---|---|---|
== | Equal | ${{ 1 == 1 }} | true |
!= | Not equal | ${{ 1 != 2 }} | true |
< | Less than | ${{ 1 < 2 }} | true |
<= | Less than or equal | ${{ 2 <= 2 }} | true |
> | Greater than | ${{ 3 > 2 }} | true |
>= | Greater than or equal | ${{ 3 >= 3 }} | true |
Values of different types are compared by type, so 1 == "1" evaluates to false.
Values of the same type follow these comparison rules:
false is less than true.null is equal to null.Logical operators use short-circuit evaluation and return one of their operands,
not necessarily a boolean. This behavior is similar to the JavaScript && and || operators.
| Operator | Description | Behavior |
|---|---|---|
|| | Logical OR | Returns the left operand if it is truthy, otherwise evaluates and returns the right operand. |
&& | Logical AND | Returns the left operand if it is falsy, otherwise evaluates and returns the right operand. |
! | Logical NOT | Returns true if the operand is falsy, false if truthy. |
The || operator is used to provide default values:
${{ inputs.name || "default" }}
If inputs.name is a non-empty string, it is returned as-is. If it is empty or null,
"default" is returned.
| Operator | Description | Example | Result |
|---|---|---|---|
+ | Unary plus | ${{ +5 }} | 5 |
- | Unary negation | ${{ -5 }} | -5 |
! | Logical NOT | ${{ !true }} | false |
Operators are listed from highest precedence to lowest. Operators on the same row have equal precedence. All binary operators are left-associative.
| Precedence | Operators |
|---|---|
| 7 (highest) | ., [], () |
| 6 | +, -, ! |
| 5 | *, /, % |
| 4 | +, - |
| 3 | ==, !=, <, <=, >, >= |
| 2 | && |
| 1 (lowest) | || |
Use parentheses to override precedence:
${{ (1 + 2) * 3 }}
Create arrays with bracket notation. Elements can be of any type and you can mix types. You can use trailing commas.
${{ [1, 2, 3] }}
${{ ["a", 1, true, null] }}
${{ [] }}
Create objects with brace notation. Keys must evaluate to strings. Values can be any type. Trailing commas are allowed.
${{ {name: "runner", version: 1} }}
${{ {"string-key": true} }}
${{ {} }}
Bare identifiers used as object keys are treated as string literals, not as variable references. To use a variable as a key, wrap it in parentheses:
${{ {name: "Alice"} }} # "name" is the string "name", not a variable reference
${{ {(obj.prop): "value"} }} # key is the value of obj.prop, which must be a string
Access object properties with dot notation:
${{ env.HOME }}
${{ steps.build.outputs.artifact_path }}
Access array elements by index, or object properties by string key:
${{ my_array[0] }}
${{ my_object["property-name"] }}
Bracket notation is required when a property name contains special characters like hyphens.
Chain property access and function calls:
${{ steps.build.outputs.items[0] }}
Call functions by name with parentheses:
${{ str(42) }}
${{ num("3.14") }}
Logical operators and the ! operator use the following truthiness rules:
| Type | Truthy when | Falsy when |
|---|---|---|
| Boolean | true | false |
| String | Length greater than 0 | Empty string "" |
| Number | Not 0 | 0 |
| Array | Length greater than 0 | Empty array [] |
| Object | Length greater than 0 | Empty object {} |
| Null | Never | Always |
str(value)Converts any value to its string representation.
${{ str(42) }} # "42"
${{ str(true) }} # "true"
${{ str(null) }} # "<null>"
num(value)Converts a string to a number. The string must be a valid numeric representation.
${{ num("42") }} # 42
${{ num("3.14") }} # 3.14
bool(value)Converts any value to a boolean based on its truthiness.
${{ bool("hello") }} # true
${{ bool("") }} # false
${{ bool(0) }} # false
${{ bool(1) }} # true
The following words are reserved and cannot be used as identifiers. They are reserved for potential future language features.
array, as, break, case, const, continue, default, else,
fallthrough, float, for, func, function, goto, if, import,
in, int, let, loop, map, namespace, number, object, package,
range, return, string, struct, switch, type, var, void, while
The keywords null, true, and false are also reserved as literal values.
deploy job:
when: manual
inputs:
environment:
default: staging
options: [staging, production]
description: Target deployment environment
strategy:
default: rolling
options: [rolling, blue-green, canary]
description: Deployment strategy
replicas:
type: number
default: 3
description: Number of replicas to deploy
image: ${{ job.inputs.environment == "production" && "deploy-tools:stable" || "deploy-tools:latest" }}
script:
- 'echo "Deploying to ${{ job.inputs.environment }} using ${{ job.inputs.strategy }}"'
- deploy
--env ${{ job.inputs.environment }}
--strategy ${{ job.inputs.strategy }}
--replicas ${{ str(job.inputs.replicas) }}
test_job:
inputs:
coverage:
type: boolean
default: false
verbose:
type: boolean
default: false
script:
- pytest ${{ job.inputs.verbose && "-v" || "" }} ${{ job.inputs.coverage && "--cov=src" || "" }}
build_job:
run:
- name: build
func: ./docker-build
inputs:
image: ${{ vars.CI_REGISTRY + "/" + vars.CI_PROJECT_PATH + ":" + vars.CI_PIPELINE_IID }}
security_scan_job:
run:
- name: scan
func: ./security-scan
- name: gate
func: ./quality-gate
inputs:
should_proceed: ${{ steps.scan.outputs.critical == 0 && steps.scan.outputs.high < 5 }}
increment_version_job:
run:
- name: current
func: ./find-version
- name: bump
func: ./bump-version
inputs:
new_version: ${{ str(steps.current.outputs.major + 1) + ".0.0" }}
deploy_job:
run:
- name: deploy
func: ./deploy
inputs:
registry: ${{ (vars.CI_COMMIT_REF_NAME == "main" && "prod.registry.com") || "staging.registry.com" }}
replicas: ${{ (vars.CI_COMMIT_REF_NAME == "main" && 5) || 2 }}
configure_job:
run:
- name: configure_ab
func: ./traffic-split
inputs:
variants: |
${{ [
{name: "control", use_new_feature: false, weight: 90},
{name: "experiment", use_new_feature: true, weight: 10}
] }}