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.

ContextAvailable inTypeEvaluatedDescription
job.inputsJob configuration: script, before_script, after_script, artifacts, cache, image, servicesObjectWhen the Runner receives the jobInput values defined for the job. Access individual variables with job.inputs.<name>.
envGitLab FunctionsObjectBefore the function runsEnvironment variables available to the function. Access individual variables with env.<name>.
inputsGitLab FunctionsObjectBefore the function runsInput values passed to the function. Access individual inputs with inputs.<name>.
varsGitLab FunctionsObjectBefore the function runsJob variables passed from the CI job. Access individual variables with vars.<name>.
stepsGitLab FunctionsObjectBefore the function runsResults from previously executed steps in the current function. Access a step’s outputs with steps.<step_name>.outputs.<output_name>.
export_fileGitLab FunctionsStringBefore the function runsPath to the file where the function can write environment variables to export to subsequent steps.
output_fileGitLab FunctionsStringBefore the function runsPath to the file where the function writes its output values.
func_dirGitLab FunctionsStringBefore the function runsPath to the directory containing the function’s definition file. Use to reference files bundled with the function.
work_dirGitLab FunctionsStringBefore the function runsPath 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:

SequenceMeaning
\\Backslash
\"Double quote
\nNewline
\rCarriage return
\tTab
\aAlert (bell)
\bBackspace
\fForm feed
\vVertical tab
\/Forward slash
\uXXXXUnicode 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:

SequenceMeaning
\\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.

OperatorDescriptionExampleResult
+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.

OperatorDescriptionExampleResult
==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: false is less than true.
  • Arrays: Element-by-element comparison.
  • Objects: Compared by length, then keys, then values. Key order does not matter.
  • Null: null is equal to null.

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.

OperatorDescriptionBehavior
||Logical ORReturns the left operand if it is truthy, otherwise evaluates and returns the right operand.
&&Logical ANDReturns the left operand if it is falsy, otherwise evaluates and returns the right operand.
!Logical NOTReturns 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

OperatorDescriptionExampleResult
+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.

PrecedenceOperators
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 string

Property 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:

TypeTruthy whenFalsy when
Booleantruefalse
StringLength greater than 0Empty string ""
NumberNot 00
ArrayLength greater than 0Empty array []
ObjectLength greater than 0Empty object {}
NullNeverAlways

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.14

bool(value)

Converts any value to a boolean based on its truthiness.

${{ bool("hello") }}  # true
${{ bool("") }}       # false
${{ bool(0) }}        # false
${{ bool(1) }}        # true

Reserved 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}
          ] }}