Moa expression language
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.
Differences from CI/CD expressions
GitLab has three expression syntaxes that serve different purposes at different stages of the pipeline lifecycle.
- Rules use their own expression syntax inside
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. - CI/CD expressions use the
$[[ ]]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. - Moa uses the
${{ }}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:
- Runtime evaluation: Step outputs do not exist until the function runs. Expressions like
${{ steps.build.outputs.image_ref }}can be evaluated only during execution. - Typed values: Moa preserves native types (numbers, booleans, arrays, and objects) and passes them between functions without converting to a string.
- Operators and logic: GitLab Functions need arithmetic (
major_version + 1), comparisons (vulnerabilities == 0), and short-circuit logic (inputs.tag || "latest") to construct step inputs from variables and outputs. - Sensitive value tracking: Moa propagates sensitive values through operations. If you concatenate a sensitive value into a string or pass it through a function call, the result is also treated as sensitive. This prevents the accidental disclosure of secrets in logs and outputs.
Context reference
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. |
Template syntax
Interpolation
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 }}!"Native type passthrough
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.
Escape Moa expressions
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.
Literals
Null
The keyword null represents the absence of a value.
${{ null }}Booleans
The keywords true and false represent boolean values.
${{ true }}
${{ false }}Numbers
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 }}Strings
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
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.
Operators
Arithmetic operators
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
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:
- Numbers: Numeric comparison.
- Strings: Lexicographic comparison (UTF-8 byte order).
- Booleans:
falseis less thantrue. - Arrays: Element-by-element comparison.
- Objects: Compared by length, then keys, then values. Key order does not matter.
- Null:
nullis equal tonull.
Logical operators
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.
Unary operators
| Operator | Description | Example | Result |
|---|---|---|---|
+ | Unary plus | ${{ +5 }} | 5 |
- | Unary negation | ${{ -5 }} | -5 |
! | Logical NOT | ${{ !true }} | false |
Operator precedence
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 }}Data structures
Arrays
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] }}
${{ [] }}Objects
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 stringProperty access
Dot notation
Access object properties with dot notation:
${{ env.HOME }}
${{ steps.build.outputs.artifact_path }}Bracket notation
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.
Chaining
Chain property access and function calls:
${{ steps.build.outputs.items[0] }}Function calls
Call functions by name with parentheses:
${{ str(42) }}
${{ num("3.14") }}Truthiness
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 |
Built-in functions
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.14bool(value)
Converts any value to a boolean based on its truthiness.
${{ bool("hello") }} # true
${{ bool("") }} # false
${{ bool(0) }} # false
${{ bool(1) }} # trueReserved words
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.
Examples
Deploy with strategy selection
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) }}Conditional flags from boolean job inputs
test_job:
inputs:
coverage:
type: boolean
default: false
verbose:
type: boolean
default: false
script:
- pytest ${{ job.inputs.verbose && "-v" || "" }} ${{ job.inputs.coverage && "--cov=src" || "" }}Building an image reference from job variables
build_job:
run:
- name: build
func: ./docker-build
inputs:
image: ${{ vars.CI_REGISTRY + "/" + vars.CI_PROJECT_PATH + ":" + vars.CI_PIPELINE_IID }}Continue gate
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 }}Version management
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" }}Environment-specific configuration
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 A/B testing
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}
] }}