Frequently Asked Questions

Common questions and answers about AnsibleForms

  1. Getting Started
    1. Deployment topology: one container or several nodes
    2. Multi-Repository Form Management
    3. About Repositories
    4. VS Code Validation for Form Files
  2. Job Management
    1. Job Relaunch with Pre-filled Data
    2. Job Log File
    3. Jobid
    4. Userinfo
    5. Userinfo Form
  3. Form Fields
    1. Cascaded Dropdowns
    2. Field Placeholders
    3. Hide a Field
    4. Group Fields
    5. Field Validation
    6. Default Value on Enum
    7. Expression Default Value
  4. Wizard & Workflows
    1. What is the difference between a wizard and a workflow?
    2. How do I conditionally show or skip wizard steps?
    3. How do I reference values from an earlier wizard step?
  5. Security & Credentials
    1. Credentials
    2. Recovering a lost admin password
    3. Restricting REST helper destinations (allow/deny lists)
    4. HashiCorp Vault Integration
  6. Integration
    1. Query AWX/Tower/AAP
  7. Customization
    1. Customization
    2. Enable YTT
  8. Access Control
    1. How do I restrict what users can do (role options)?
    2. How do I implement custom RBAC logic in my playbooks or forms?
  9. Job Scheduling
    1. How do I schedule a form to run automatically?
    2. Which cron expressions can I use?
  10. Save & Load Form Data
    1. How do I save and reload form data without running a job?
  11. Nested Forms & Structured Fields
    1. How do I collect structured or repeated data in a form?
    2. How do I access parent form data inside a subform?
    3. How do I migrate from table / tableFields to list / subform?

Getting Started

Deployment topology: one container or several nodes

By default, one AnsibleForms container runs the web application and the background work in one process, next to an RTE container that runs the playbooks. Run it with a restart policy (for example restart: unless-stopped in Docker), and back up both the database and the persistent volume.

To run several replicas behind a load balancer, split the process with AF_ROLE:

  • app nodes serve the web interface and the API: run as many as you like, each with the same ACCESS_TOKEN_SECRET
  • a worker runs the background work: the schema and its upgrades, the config seed, the schedules, the nightly backup, the repository syncs and the cleanups. One worker works at a time, holding a database lock; a second one waits and takes over when the first stops
  • rte containers run the playbooks: run more of them to run more playbooks at once

The nodes share the database, the persistent volume and ENCRYPTION_SECRET. The chat assistant needs sticky sessions, and each node keeps its own log. See Scaling and the examples/scale setup.

Multi-Repository Form Management

Forms from every Git repository with use for forms enabled are merged automatically; on duplicate form names the first one wins and a warning is logged. Keep config.yaml in only one repository. See Git repositories.

About Repositories

AnsibleForms clones and pulls Git repositories under the repositories path (REPO_PATH), managed under Settings > Repositories. Switches on each repository set what it holds: config, forms, playbooks or vars files. See Git repositories.

VS Code Validation for Form Files

Add a schema header to your form YAML files to get live validation and autocomplete in VS Code.

Install the YAML extension and add this comment as the first line of any form file:

# yaml-language-server: $schema=https://raw.githubusercontent.com/ansibleforms/ansibleforms/main/server/schema/public/form_schema.json

This works for both single-form files (a YAML dict) and multi-form files (a YAML list):

# yaml-language-server: $schema=https://raw.githubusercontent.com/ansibleforms/ansibleforms/main/server/schema/public/form_schema.json
- name: My Form
  type: ansible
  playbook: site.yml
  roles: [default]
  fields:
    - name: env
      type: enum
      values: [dev, staging, prod]

VS Code highlights unknown properties, missing required fields and incorrect types as you type.


Job Management

Job Relaunch with Pre-filled Data

The relaunch button opens the form pre-filled with the values of the earlier job; password fields are never stored. It requires the role option allowJobRelaunch, and a form can turn it off with allowRelaunch: false. See Relaunch a job.

Job Log File

Track progress via a job-specific log file (v6.1.2).

AnsibleForms can display a job-specific log file alongside the Ansible output. This lets you track the progress of long-running custom modules that would otherwise produce no visible feedback during execution.

This feature only works for playbooks that run on an RTE. Templates and jobs on AWX, AAP, Ascender, Semaphore, Rundeck or Jenkins have no log file.

How it works:

  • During a job run, the RTE watches for a log file at a well-known path inside the playbook folder
  • Every two seconds, when the file changed, the RTE copies its contents into the job, and once more when the playbook ends
  • The log file is displayed in a Log file section of the job’s output, numbered as the output’s lines : a log line’s timestamp and level (ERROR, WARNING, INFO, DEBUG) are read and coloured as the output’s, and ANSI colours are supported

Log file path convention:

The log file must be written to this path:

<playbook_dir>/.joblogs/job_log_<jobid>.log

AnsibleForms passes __jobid__ automatically, so you can assemble the path in your playbook without any extra configuration:

job_log_path: "{{ vars['playbook_dir'] }}/.joblogs/job_log_{{ __jobid__ }}.log"

Pass this variable to any role or custom module that should write progress to the file.

Responsibilities of the playbook / module developer:

  • Create the log file (and its .joblogs/ directory) at the start of the operation
  • Write meaningful progress messages as the operation proceeds

The RTE deletes the file once the playbook ends and the last contents are stored on the job.

ANSI colour coding is supported.
Standard ANSI escape codes (for example, \033[92m for green and \033[91m for red) in your log messages are rendered as colours in the UI, so success, warning and error states can be distinguished at a glance.

Typical use case:
A custom Python Ansible module that performs a long sequence of API calls or data operations (for example, a SnapMirror DR workflow) can append one line per step to the log file. An operator watching the job in AnsibleForms then sees live progress in the Log file section as the job page follows the job.

Jobid

Pass the current job ID.

AnsibleForms automatically sends the current job ID in the extravars as __jobid__; no configuration is required.

Userinfo

Pass the current user.

AnsibleForms automatically sends the user information in the extravars as ansibleforms_user; no configuration is required.

You can choose how much of it is sent. EXTRAVARS_USER_FIELDS takes a comma-separated list of top-level keys (username,email,type), or none to send nothing; a form can override it with its own userExtravars property. The default is the whole object, as before. The form-side __user__ described below comes from the login token and is never affected.

Userinfo Form

Access current user info in the form (v4.0.2).

The field __user__ is automatically added to the form and can be referenced in expressions:

expression: $(__user__)
expression: "'$(__user__.username)'"
expression: $(__user__.groups)
expression: $(__user__.roles)

Form Fields

Cascaded Dropdowns

Make cascaded dropdowns.

enum fields (dropdown boxes) can contain placeholders in their query or expression property, in the format $(another_field) or $(another_field[0].name).
As soon as the referenced field changes, the referencing field is re-evaluated, which results in dynamic, cascading dropdown boxes.
The client web application re-evaluates fields every 100 ms; with current processors and the Chromium engine, this makes for a seamless experience.

A reference to another enum field returns the selected values, not the full dropdown list. Use the placeholderColumn property or dot notation such as $(city.name).
New in v4.0.20: setting placeholderColumn to “*” outputs the entire record instead of a single column.

- type: enum
  dbConfig: 
    name: CONN1
    type: mysql
  query: select name from cmdb.city
  name: city_name
  label: Select a city
  default: Amsterdam
  required: true
  model: cmdb.city
  group: CMDB
- type: enum
  dbConfig: 
    name: CONN1
    type: mysql
  query: select datacenter.name from cmdb.datacenter,cmdb.city where datacenter.city_id=city.id
    and city.name='$(city_name)'
  name: datacenter_name
  label: Select a datacenter
  default: __auto__    # default can be "__auto__" (first item) or "__all__" (all items) or "__none__" (no default)
  multiple: true
  outputObject: true
  required: true
  model: cmdb.datacenter
  group: CMDB      

# or use the placeholderColumn property

- type: enum
  dbConfig: 
    name: CONN1
    type: mysql
  query: select id,name,description from cmdb.city
  name: city
  label: Select a city
  default: Amsterdam         # evaluated against valueColumn
  required: true
  model: cmdb.city
  group: CMDB
  valueColumn: name          # we choose the name column as value for placeholders
  placeholderColumn: id      # we can reference the id by using $(city)
  previewColumn: description # when you select a value, the dropdown will show description
  columns:                   # we hide id
  - name
  - description
- type: enum
  dbConfig: 
    name: CONN1
    type: mysql
  query: select name, capacity_pct from cmdb.datacenter where datacenter.city_id=$(city)
# this cascaded dropdown will react using "id" as placeholder
# or "query":"select name from cmdb.datacenter where datacenter.city_id=$(city.id)",  
# or reference the column in the placeholder                            
  name: datacenter_name
  label: Select a datacenter
  default: __auto__         # default can be "__auto__" (first item) or "__all__" (all items) or "__none__" (no default)
  multiple: true
  pctColumns:
  - capacity_pct
  outputObject: true
  required: true
  model: cmdb.datacenter
  group: CMDB

Field Placeholders

Reference another field’s value.

Placeholders are references to other fields in the form.
A placeholder always has the format $(reference), and both expressions and queries can contain placeholders.
A placeholder that points to a simple field (text, number, password) holds that field’s value.
A placeholder that points to an object-based enum field must use either the placeholderColumn property or dot notation such as $(city.name).
A placeholder that points to an expression field returns the full object, or you can use an advanced, JavaScript-like reference such as $(myarray[0].name).

The placeholder is replaced before the expression is evaluated. If you expect the result to be a string, you must wrap it in quotes.
New in v4.0.20: setting placeholderColumn to “*” outputs the entire record instead of a single column.

- name: field1
  type: expression
  expression: "[{name: 'foo'},{name: 'bar'},{name: 'ansible'}]"
  runLocal: true
- name: field2
  type: expression
  expression: "'$(field1[0].name)'"  # result : 'foo' (note the wrapping quotes)
  runLocal: true
- name: field3
  type: expression
  expression: "$(field1)[0].name"  # result : {name: 'foo'}.name => 'foo'  
  runLocal: true    
- name: field4
  type: expression
  expression: "$(field1).slice(-1)[0].name"  # result : {name: 'ansible'}.name => 'ansible' 
  runLocal: true
- name: field5
  type: enum
  expression: $(field1).filter(x => x.name.includes('a'))   # result : [{name: 'bar'},{name: 'ansible'}]
  runLocal: true
  default: __auto__                                         # result: bar
- name: field6
  type: expression
  expression: "'$(field5)'"   # result : the selected item from field5 (default=bar)
  runLocal: true        

Hide a Field

Hide a field.

Hide a field with the field property hide, or show and hide it dynamically with the field properties dependencies and dependencyFn.

Group Fields

Group fields together in a block.

Use the field property group. Fields with the same group name are grouped in a block.

Field Validation

Validate a field.

Use the validation field properties, such as regex, minValue and notIn.

Default Value on Enum

Enum default value.

The field property default sets a default value, and on enum fields you can use values such as __auto__ to select the first item automatically.

For a dynamic default based on an expression, move the default item to the top of the list, as in the following example.

# using client javascript manipulation

- type: expression
  expression: "[{name:'bert'},{name:'ernie'},{name:'pino'}]"
  name: dropdownsource
  label: Dropdown source
  runLocal: true
- type: expression
  expression: "'pino'"
  name: dropdownsourceDefault
  label: Default source
  runLocal: true
- type: enum
  expression: "[...[{name:'$(dropdownsourceDefault)'}],...$(dropdownsource).filter(x => x.name!=='$(dropdownsourceDefault)')]"
  name: dropdownwithdefault
  label: Example with expression default by moving it to top
  runLocal: true
  default: __auto__     

# explained : 
# we take our default and merge it with the source where we filter out the default (to avoid doubles)    
# the default is thus shifted to the first element, which we can now select with the default `__auto__`

Expression Default Value

Expression default value.

For text, number or date fields, the default property sets a static default value.

To make the default dynamic, based on an expression, use one of two properties:

  • editable: use the editable property to make an expression-field editable
  • evalDefault: use the evalDefault property to evaluate the default as an expression
# using editable
- type: expression
  expression: "'$(some_other_field)'.toLowerCase()"
  name: field1
  runLocal: true
  editable: true # this will add an edit-button so you manually overwrite the expression value

# using evalDefault
- type: text
  name: field1
  default: "'$(some_other_field)'.toLowerCase()"
  evalDefault: true # it will treat the default as if it was an expression.
  # note : when `some_other_field` changes, the default will be re-evaluated
  # note2 : works for other fields too.

# more complex evalDefault example
- name: checkbox
  type: checkbox
- name: textfield1
  type: text
  line: line2
  default: ping
- name: textfield2
  type: text
  line: line2
  default: pong
- name: checkbox3
  type: checkbox
  line: line3
  label: This checkbox will default check if checkbox or textfield1==textfield2
  default: |
    (
      (c=false,t1="",t2="") => { return c || (t1==t2) }
    )( $(checkbox) , "$(textfield1)" , "$(textfield2)" )
    
  evalDefault: true

Wizard & Workflows

What is the difference between a wizard and a workflow?

A wizard and a workflow both come in steps, but they operate on different layers and can be combined.

  workflow (Workflow) wizard
Layer Execution Form / UI
What it splits The job into nodes that run one after the other, or side by side The input of one form into multiple pages
Result 1 workflow job, with a job per node (each with its own playbook or template) 1 job runs at the end
Form type Must be workflow Works with every form type except subform
Defined by A workflow file of the designer (workflow:) A list of subform references (wizard:)
Use it when “Do A, then B, then C as separate jobs” “My form has too many fields for one page”

Key takeaway: a workflow is about what runs and in which order; wizard is about how the user fills in the form.

The multistep form type, with its steps, did the execution part before 7. It is deprecated since 7 and removed in 8: a workflow does all a multistep does, see Multistep forms.

Workflow only

A type: workflow form runs one of the designer’s workflows. The user fills in a single page of fields and presses Submit, and the workflow runs its nodes, each with the form’s values as extra vars:

- name: Provision host
  type: workflow
  workflow: provision-host
  roles:
    - default
  fields:
    - name: hostname
      type: text

Wizard only

A wizard: block on a form spreads input collection over multiple pages. Only one job runs at the end: the merged extravars from all pages are sent to a single playbook, template or workflow.

- name: Provision host
  type: ansible
  roles:
    - default
  playbook: provision.yml
  wizard:
    - subform: basics
      title: Basics
    - subform: network
      title: Network
      defaultModel: net

- name: basics
  type: subform
  fields:
    - name: hostname
      type: text
      required: true

- name: network
  type: subform
  fields:
    - name: ipv4
      type: text

A read-only review page is appended automatically as the last wizard page; you do not declare it in YAML.

Combined (wizard on top of a workflow)

A wizard can be layered on top of a workflow form. The user fills in the wizard pages and presses Submit, and then the workflow starts. By matching a wizard step’s defaultModel with a node’s extra_vars_key, you can route one wizard page to one node:

- name: Provision and verify host
  type: workflow
  workflow: provision-host
  roles:
    - default
  wizard:
    - subform: basics
      title: Basics
      defaultModel: basics            # wraps basics fields under `basics`
    - subform: network
      title: Network
      defaultModel: network           # wraps network fields under `network`

And the workflow provision-host, in a workflow file of the designer:

workflow_job_templates:
  - name: provision-host
    related:
      workflow_nodes:
        - identifier: create
          unified_job_template:
            name: create_host.yml
            type: job_template
          extra_vars_key: basics      # only sees the basics page payload
          related:
            success_nodes:
              - identifier: network
        - identifier: network
          unified_job_template:
            name: configure_network.yml
            type: job_template
          extra_vars_key: network     # only sees the network page payload

extra_vars_key is a single-level lookup, so use a flat name in defaultModel (for example, defaultModel: basics, not defaultModel: input.basics) when you want them to match.

See the Wizard page for the full property reference.

How do I conditionally show or skip wizard steps?

Use when: to hide a step entirely, or optional: true to let the user skip a visible step. The two are independent and should generally not be combined.

when:: conditional visibility

The step is hidden when the expression evaluates to a falsy value. The user never sees it, and its values are not collected. The expression can read earlier steps through __parent__.<stepname>.<field>:

wizard:
  - subform: basics             # has a `kind` enum field with values vm/bare-metal/container
    title: Basics
  - subform: virtualization
    title: Virtualization
    when: $(__parent__.basics.kind) === 'vm'
  - subform: hardware
    title: Hardware
    when: $(__parent__.basics.kind) === 'bare-metal'

Only the page matching the chosen kind is shown. Steps after a hidden one are renumbered automatically.

optional: true: allow skipping a visible step

The step is always shown in the stepper, but the user can press Next without filling it in, and Submit is allowed even if the page was never visited or completed:

wizard:
  - subform: basics
    title: Basics
  - subform: advanced
    title: Advanced (optional)
    optional: true

The user can open the Advanced page, leave it empty, press Next and proceed directly to the review page.

when: controls visibility; optional: controls whether the page must be completed. Do not combine them: to make a page disappear, use when:; to keep it visible but skippable, use optional:.

How do I reference values from an earlier wizard step?

Use $(__parent__.<stepname>.<field>) inside any field of a later step.

The name of a wizard step (defaults to its subform name) is the namespace under which its values are exposed to later steps:

- name: Provision host
  type: ansible
  playbook: provision.yml
  roles:
    - default
  wizard:
    - subform: basics          # step name defaults to "basics"
      title: Basics
    - subform: network
      title: Network

- name: basics
  type: subform
  fields:
    - name: hostname
      type: text
      required: true

- name: network
  type: subform
  fields:
    - name: fqdn
      type: text
      # Re-evaluated when basics.hostname changes
      default: $(__parent__.basics.hostname).local

This is the same __parent__ mechanism that list and yaml subforms use; see How do I access parent form data inside a subform?.


Security & Credentials

Credentials

Pass credentials.

Credentials can be passed in several ways:

  • using the field property asCredential
  • using the credentials form property (key-value pairs)
  • using an extravar called __credentials__
# assume you have 2 credentials created in Ansible Forms
# 1: vcenter
# 2: ad

# you want them exposed to the playbook as
# 1: vc_cred
# 2: ad_cred

# Method 1 : using asCredential field-property
fields:
- name: vc_cred
  type: expression
  runLocal: true
  asCredential: true
  expression: "'vcenter'"
- name: ad_cred
  type: expression
  runLocal: true
  asCredential: true
  expression: "'ad'"

# Method 2 : using credentials form-property   
name: myplaybook
type: ansible
playbook: myplaybook.yaml
roles:
  - default
credentials:
  vc_cred: vcenter
  ad_cred: ad
  veeam_cred : veeam_prod,veeam_dev # will first try veeam_prod, then as fallback veeam_dev

# Method 3 : using __credentials__ extravar
fields:
- name: __credentials__
  type: expression
  runLocal: true
  expression: "{vc_cred: 'vcenter',ad_cred: 'ad',veeam_cred:'$(veeam_server)'}"
  # note : in the expression you can use placeholders to make them dynamic

Recovering a lost admin password

If you have lost the password of the local admin account and no other admin user is available, use the REINIT_ADMIN recovery option. This is not a runtime authentication bypass: it only forces a one-time reset of the local admin account at startup, after which normal authentication proceeds.

How it works

When REINIT_ADMIN=1 is set at startup, AnsibleForms will:

  1. Ensure the admins group exists (creating it if missing).
  2. Look up the local admin user (default username admin, overridden by ADMIN_USERNAME).
  3. If it exists, reset its password to the value of ADMIN_PASSWORD and re-attach it to the admins group.
  4. If it does not exist, create it (as on a fresh install).
  5. Log the action prominently so that it is visible in the logs.

Steps

  1. Stop AnsibleForms.
  2. Set the environment variables (use a strong password):
    ADMIN_USERNAME=admin
    ADMIN_PASSWORD=YourNewStrongPasswordHere
    REINIT_ADMIN=1
    

    In Docker Compose, add them to the environment: block of the AnsibleForms service.

  3. Start AnsibleForms and watch the logs for a line such as:
    REINIT_ADMIN: admin user 'admin' has been recreated. UNSET REINIT_ADMIN now.
    
  4. Log in with admin / YourNewStrongPasswordHere.
  5. Unset REINIT_ADMIN (or set it back to 0) and restart, so that future restarts do not reset the admin password again.

Notes

  • If you were locked out because LDAP was the only configured login method and it failed, the recovered local admin account always falls back to local database authentication, which is enough to log in and fix LDAP.
  • The former ENABLE_BYPASS environment variable has been removed. It allowed login as admin with any password and was unsafe to leave enabled. REINIT_ADMIN only resets the password; normal authentication applies afterwards.
  • REINIT_ADMIN does not invalidate existing sessions and tokens; only the password hash and group membership change.

Restricting REST helper destinations (allow/deny lists)

The fn.fnRestBasic, fn.fnRestAdvanced, fn.fnRestJwt and fn.fnRestJwtSecure helpers can be called from form expression properties to fetch data over HTTP. By default, expression authors can target any URL that the AnsibleForms host can reach, which you may want to limit on a sensitive network.

Two environment variables provide an allow list and a deny list:

Variable Behaviour
REST_ALLOWED_HOSTS Comma-separated hostnames or CIDRs. When set, only these targets are allowed.
REST_DENIED_HOSTS Comma-separated hostnames or CIDRs. Always blocked. Takes precedence over the allow list.

Hostnames are matched case-insensitively against the URL host. CIDRs are matched against every IP the host resolves to, so 10.0.0.0/8 blocks any DNS name that resolves into the private range.

Examples:

The first line allows only two hosts; the second blocks specific targets:

# Whitelist: only your two API partners are reachable
REST_ALLOWED_HOSTS=api.example.com,partner.api.com

# Blacklist: block cloud metadata + internal admin UIs
REST_DENIED_HOSTS=169.254.169.254,127.0.0.0/8,internal-admin.example

This guard only protects the AnsibleForms Node.js process. Once a playbook runs, Ansible itself can reach anything from the host, outside the control of AnsibleForms. Use these lists to prevent form authors from turning expressions into a proxy for metadata services or internal UIs; do not rely on them as a network firewall.

HashiCorp Vault Integration

Credentials can read their user and password from HashiCorp Vault, configured as a secret store. See Secret stores. The VAULT_* environment variables of earlier versions are imported once as the store vault at the first 7.x start.

For secrets used only inside one playbook, you can also use the community.hashi_vault lookup plugin, which reads from Vault directly and bypasses AnsibleForms.


Integration

Query AWX/Tower/AAP

Query information from AWX or Ansible Automation Platform.

To populate dropdown boxes with data from AWX or Tower, use fn.fnRestBasic or fn.fnRestJwtSecure.

name: Query awx
type: awx
runner: myAwxConfigName # a runner of type awx
template: my_template # will be overwritten by the field __template__
description: ""
awxCredentials:
  - vmware
executionEnvironment: my_execution_environment
roles:
  - default
categories: []
inventory: my_inventory # will be overwritten by the field __inventory__
tileClass: bg-info-subtle
icon: bullseye
fields:
  # make sure you add credentials called "awx_rest" where the password holds the token
  # if you like basic authentication, switch the expressions below to fn.fnRestBasic instead

  - name: organization
    label: Organization
    type: enum
    default: __auto__
    expression: "fn.fnRestJwtSecure('get','https://172.16.50.1/api/v2/organizations','','awx_rest','[.results[]]')"
    columns:
      - name
    valueColumn: id # => we want the organisation field to hold the id !!
  - name: __template__  # use this special name to override the template from the form
    label: Template
    type: enum
    default: __auto__
    expression: "fn.fnRestJwtSecure('get','https://172.16.50.1/api/v2/job_templates?organization=$(organization)','','awx_rest','[.results[]]')"
    columns:
      - name
    valueColumn: name
  - name: __inventory__  # use this special name to override the inventory from the form
    label: Inventory
    type: enum
    default: __auto__
    expression: "fn.fnRestJwtSecure('get','https://172.16.50.1/api/v2/inventories?organization=$(organization)','','awx_rest','[.results[]]')"
    columns:
      - name
    valueColumn: name    
  - name: __awxCredentials__  # use this special name to override the credentials from the form
    label: Credentials
    type: enum
    expression: "fn.fnRestJwtSecure('get','https://172.16.50.1/api/v2/credentials?organization=$(organization)','','awx_rest','[.results[]]')"
    multiple: true
    default: __auto__
    columns:
      - name
    valueColumn: name     
  - name: __executionEnvironment__  # use this special name to override the executionEnvironment from the form
    label: Execution environment
    type: enum
    expression: "fn.fnRestJwtSecure('get','https://172.16.50.1/api/v2/execution_environments?organization=$(organization)','','awx_rest','[.results[]]')"
    default: __auto__
    columns:
      - name
    valueColumn: name           

Customization

Customization

Customize AnsibleForms.

AnsibleForms is a web application. When you run it natively in Node.js, you can replace or change its files.
The recommended approach, however, is to run it as a Docker image and add volume or file mappings. The Docker Compose projects already map directories to keep the database, playbooks, logs, certificates and SSH keys persistent, and you can add more mappings. A custom logo is uploaded on the Logo settings page.
The custom.js file holds your own JavaScript functions for use in expressions. The built-in functions use the prefix fn. (for example, fn.fnRestBasic); custom functions use the prefix fnc..
In the same way, you can add your own jq definitions in the jq.custom.definitions file.

volumes:
  # Mount application folder to host folder (to maintain persistency)
  - ./data:/app/dist/persistent
  # Map custom functions for js expressions and jq
  - ./data/functions/custom.js:/app/dist/src/functions/custom.js
  - ./data/functions/jq.custom.definitions:/app/dist/src/functions/jq.custom.definitions
  # Map custom sshkey to local node .ssh location
  - ./data/ssh:$HOME_DIR/.ssh
  - ./data/git/.gitconfig:$HOME_DIR/.gitconfig

Enable YTT

Enable ytt.

To use ytt, set USE_YTT=1.
For more information about ytt, see https://carvel.dev/ytt/.

This feature has not been extensively tested, and no feedback has been received since it was added as an enhancement.
When using ytt, you must disable the designer: the designer converts the YAML files to intermediate JSON and drops the ytt syntax (which consists of YAML comments).

A lib directory must exist in the root directory; it is automatically included in the ytt call. Data can be provided globally by setting prefixed environment variables:

YTT_VARS_PREFIX=YTT_VAR
YTT_VAR_INVENTORY_PATH=/tmp/inventory.yml
YTT_VAR_default_host=localhost

Alternatively, provide library data files:

YTT_LIB_DATA_DEMO=/tmp/demo_data.yml
# /tmp/demo_data.yml
message: 'hello demo'

The library demo must exist in the ytt context (lib/_ytt_lib/demo/values.yml)

# lib/_ytt_lib/demo/values.yml
#@ data/values
---
demo: {}

The loaded data can then be used:

# config.yaml
#@ load("@ytt:data", "data")
#@ load("@ytt:library", "library")
#@ demo = library.get("demo")
---
categories:
  - name: Default
    icon: bars
roles:
  - name: admin
    groups:
      - local/admins
constants:
  data_values: #@ data.values
  demo: #@ demo.data_values()

Access Control

How do I restrict what users can do (role options)?

Beyond restricting which forms a role can see, the options of a role switch UI features on or off for its members. See Role options for every option, its default and how options combine across roles.

How do I implement custom RBAC logic in my playbooks or forms?

User identity is available in both the frontend and the backend at every execution.

On every form submission, AnsibleForms automatically injects the current user’s full identity into the extravars sent to Ansible:

ansibleforms_user:
  username: jane.doe
  groups:
    - local/admins
    - ldap/network-team
  roles:
    - admin
    - operators
  options:
    showLogs: true
    allowJobRelaunch: true
    # ...all resolved role options

Your playbook or any custom Ansible module can therefore use ansibleforms_user directly for fine-grained decisions, for example to allow only certain groups to modify the production inventory, or to write an audit trail with the submitter’s username.

In the frontend, the same object is available through the special __user__ field:

fields:
  - name: is_admin
    type: expression
    runLocal: true
    hide: true
    expression: "$(__user__.roles).includes('admin')"

  - name: target_env
    type: enum
    # hide the production option for non-admins by cross-referencing __user__
    expression: |
      $(__user__.roles).includes('admin')
        ? ['dev','staging','production']
        : ['dev','staging']
    runLocal: true
    default: __auto__

Typical patterns:

The user identity supports patterns such as the following:

  • Cross-reference an RBAC config file or database: load a YAML or JSON file (with fn.fnReadYamlFile or an expression) that maps groups to allowed resources, then filter on $(__user__.groups)
  • Audit trail: pass ansibleforms_user.username as an extra variable to record who triggered the job
  • Dynamic field values: show a different set of enum choices, pre-fill fields, or hide sections based on the user’s groups or roles
  • Playbook-side authorization: assert that ansibleforms_user.groups contains a required group before the playbook proceeds, as a defence-in-depth check independent of the form’s roles list

The object above is what is sent by default. If the instance sets EXTRAVARS_USER_FIELDS, or the form sets userExtravars, only the keys named there are sent, so a playbook that asserts on ansibleforms_user.groups requires groups to be one of them. The frontend __user__ field comes from the login token and is never trimmed by either setting.


Job Scheduling

How do I schedule a form to run automatically?

Open the dropdown next to Submit and choose Schedule (Recurring) or Run Later (One-time); they require the role options allowScheduledJobs (admin-level) and allowPlannedJobs. See Schedules.

Which cron expressions can I use?

A recurring schedule takes a standard 5-field cron expression (minute, hour, day of month, month, day of week).

Expression Runs
*/15 * * * * Every 15 minutes
0 * * * * Every hour, on the hour
0 */6 * * * Every 6 hours, at 00:00, 06:00, 12:00 and 18:00
0 2 * * * Every day at 02:00
30 7 * * 1-5 Monday to Friday at 07:30
0 8 * * MON Every Monday at 08:00 (day and month names are accepted)
0 0 1 * * At midnight on the first day of every month
0 22 L * * At 22:00 on the last day of every month
0 6 * * 1#1 At 06:00 on the first Monday of every month
*/30 * * * * * Every 30 seconds (an optional sixth field in front is the second)

When both the day of the month and the day of the week are set, the schedule runs when either matches: 0 0 13 * 5 runs on the 13th and on every Friday. The server refuses an expression it cannot run, or one that can never occur (0 0 30 2 *).

On the Schedules page, the cron editor describes the expression and previews its next runs. It does not accept nicknames such as @daily, the ? wildcard or the W (nearest weekday) modifier, so keep to the forms in the table.

Time zone Cron schedules run in the server’s time zone, set with LOG_TZ (default UTC), not in the time zone of your profile or your browser. 0 2 * * * with LOG_TZ=Europe/Madrid runs at 02:00 Madrid time all year, following daylight saving time. The cron editor previews the next runs in that same zone. Repository pull schedules use the same zone.


Save & Load Form Data

How do I save and reload form data without running a job?

Select Store in the dropdown next to Submit to save the field values, then Load from Store to restore them later. Password fields are never stored. It requires the role option allowStoredJobs. See Stored jobs.


Nested Forms & Structured Fields

How do I collect structured or repeated data in a form?

Use list and yaml fields with subforms (6.2.0+).

To collect complex structured data, such as a list of servers, a set of network interfaces or a single nested object, use the list or yaml field type together with a subform.

A subform is a reusable form fragment (defined with type: subform) that is never shown as a standalone tile. It exists solely to be referenced by fields in other forms. See the Subform docs for the full reference.

Collecting a list of structured rows: list field:

A list field opens the referenced subform once per row:

- name: Server
  type: subform
  fields:
    - name: hostname
      type: text
      required: true
    - name: ip
      type: text
      regex:
        expression: ^\d+\.\d+\.\d+\.\d+$
        description: Must be an IPv4 address

- name: Deploy to servers
  type: ansible
  roles:
    - default
  playbook: deploy.yml
  fields:
    - name: servers
      type: list
      subform: Server   # opens Server subform in a drilldown editor per row

The servers extravar sent to Ansible is an array of objects: [{hostname: "web1", ip: "10.0.0.1"}, ...]

Editing a single structured object: yaml field:

The yaml field has three modes:

Mode How Behaviour
Editor default Shows a full YAML syntax-highlighted editor the user can type in directly
Readonly readonly: true Renders the YAML value as formatted read-only text, no editing
Subform subform: MySubform Hides the raw editor; opens the subform as a drilldown editor on click
fields:
  # editor mode (default)
  - name: raw_config
    type: yaml

  # readonly mode
  - name: generated_config
    type: yaml
    readonly: true

  # subform mode
  - name: network_config
    type: yaml
    subform: NetworkConfig   # opens NetworkConfig subform as a drilldown editor

Upload and download: list and yaml fields:

Both field types support client-side file transfer through two optional properties:

fields:
  - name: servers
    type: list
    subform: Server
    showLoadButton: true      # shows an Upload button: imports content from a local file
    showDownloadButton: true  # shows a Download button: exports current content to a file

  - name: config
    type: yaml
    showLoadButton: true
    showDownloadButton: true

The list field and the subform form type replaced the table field and tableFields, which were removed in 7.0.0.

How do I access parent form data inside a subform?

Reference parent field values from within a subform via __parent__ (v6.3.0+).

When a subform opens, whether from a list field (each row editor) or a yaml field in subform mode, AnsibleForms automatically injects a special read-only field called __parent__ into the subform. It contains a snapshot of every field value in the parent form at the time the subform was opened, including constants and vars.

This lets subform fields use expressions that reference parent data without any extra configuration.

What is in __parent__?

__parent__ is a plain object whose keys are the field names of the parent form:

__parent__:
  environment: production          # a regular field
  region: eu-west-1
  max_nodes: 10
  owner: jane.doe                  # a constant
  default_image: ubuntu-22.04      # from varsFiles
  __user__:                        # system fields are also included
    username: jane.doe
    roles: [admin]

__parent__ is not sent to Ansible; like __user__, it is stripped from the extravars. It is purely a frontend helper for expressions inside subforms.

Accessing parent values in subform expressions

Reference parent values with the standard $(...) expression syntax:

- name: NodeConfig
  type: subform
  fields:
    - name: node_name
      type: text
      label: Node name
      required: true

    - name: image
      type: enum
      label: Image
      # default to the parent form's chosen image
      expression: "'$(__parent__.default_image)'"
      runLocal: true

    - name: is_production
      type: expression
      hide: true
      runLocal: true
      expression: "'$(__parent__.environment)' === 'production'"

    - name: node_type
      type: enum
      # only offer gpu nodes in production
      expression: |
        '$(__parent__.environment)' === 'production'
          ? ['standard', 'high-memory', 'gpu']
          : ['standard', 'high-memory']
      runLocal: true
      default: __auto__

- name: Deploy cluster
  type: ansible
  roles:
    - default
  playbook: deploy_cluster.yml
  fields:
    - name: environment
      type: enum
      values: [dev, staging, production]

    - name: default_image
      type: text
      default: ubuntu-22.04

    - name: nodes
      type: list
      subform: NodeConfig
      columns: [node_name, node_type]

Nested subforms

__parent__ always refers to the immediate parent form. If you nest a list inside a subform that is itself opened from a parent form, the inner subform’s __parent__ holds the middle subform’s data. To reach further levels, chain references such as $(__parent__.__parent__.someField) if the middle subform also propagates its own __parent__.

How do I migrate from table / tableFields to list / subform?

The table field and the tableFields property were deprecated in 6.2.0 and removed in 7.0.0, and a form that still uses them fails validation. To migrate:

  1. Extract the columns from tableFields into a new type: subform form with regular formfields
  2. Replace the table field with a list field that references the subform via subform: MySubformName

Before:

- name: Manage users
  type: ansible
  roles:
    - default
  playbook: users.yml
  fields:
    - name: users
      type: table
      tableFields:
        - name: username
          type: text
        - name: email
          type: text

After:

- name: User
  type: subform
  fields:
    - name: username
      type: text
    - name: email
      type: text

- name: Manage users
  type: ansible
  roles:
    - default
  playbook: users.yml
  fields:
    - name: users
      type: list
      subform: User

Migrating from in tableFields to __parent__ expressions

The from property of tableFields enum columns populated dropdown choices from another field in the parent form. In a subform, it is replaced by an expression that reads the same value through __parent__.

Before: tableFields with from:

- name: Manage members
  type: ansible
  roles:
    - default
  playbook: members.yml
  fields:
    - name: available_departments
      type: expression
      expression: "['HR','Engineering','Finance']"
      runLocal: true
      hide: true

    - name: members
      type: table
      tableFields:
        - name: department
          type: enum
          from: available_departments   # pulls choices from the parent field above
        - name: firstname
          type: text

After: subform with __parent__ expression:

- name: Member
  type: subform
  fields:
    - name: department
      type: enum
      # replaces "from: available_departments"
      expression: "$(__parent__.available_departments)"
      runLocal: true

    - name: firstname
      type: text

- name: Manage members
  type: ansible
  roles:
    - default
  playbook: members.yml
  fields:
    - name: available_departments
      type: expression
      expression: "['HR','Engineering','Finance']"
      runLocal: true
      hide: true

    - name: members
      type: list
      subform: Member

$(__parent__.available_departments) returns the current value of that field, so if that field is itself a dynamic expression field, the subform always sees the latest evaluated result from the parent.