Use multiple git repositories for forms
AnsibleForms supports loading forms from multiple git repositories simultaneously. Forms from all repositories with the “Use for forms” switch enabled are automatically merged together.
How it works:
- Configure multiple repositories in Settings → Repositories
- Enable the “Use for forms” switch on each repository you want to load forms from
- Each repository can contain a
forms/directory with form YAML files - All forms from all repositories are merged together automatically
- Forms with duplicate names will trigger a warning (first one wins)
Configuration File Discovery:
- AnsibleForms will use the FIRST config.yaml (or config.yaml) found across all form repositories
- If multiple config files are found, a warning is logged
- You can have a single central config repository for shared categories/roles/constants
- Or use the local
persistent/config.yamlfile (checked if no repository config found)
Best Practices:
- Keep config.yaml in only ONE repository or use the local persistent/config.yaml
- Organize forms by team/project using separate repositories
- Use unique form names across all repositories to avoid conflicts
- Repository order matters: forms are loaded in database order
Example Setup:
Repository 1 (Central Config):
- config.yaml (categories, roles, constants)
Repository 2 (Network Team):
- forms/
- switch_config.yaml
- router_setup.yaml
Repository 3 (Server Team):
- forms/
- server_deploy.yaml
- backup_restore.yaml
All forms appear together in the UI, automatically merged!
Relaunch jobs with form data
AnsibleForms supports relaunching jobs with pre-filled form data. When you click the relaunch button in the jobs page, the form will open with all field values from the previous job submission.
Security Features:
- Password fields are automatically excluded from storage and retrieval for security
- Raw form data is stored separately from processed extravars (before model transformations)
Permission Control:
Forms can disable relaunch functionality using the disableRelaunch option:
- name: Production Deployment
disableRelaunch: true # Prevents relaunching this form
Users must have the allowJobRelaunch role option enabled (admins have this by default):
roles:
- name: operators
options:
allowJobRelaunch: true # Allow this role to relaunch jobs
Most Restrictive Logic: Relaunch is only available if BOTH conditions are met:
- Form does NOT have
disableRelaunch: true - User role has
allowJobRelaunch: true(or user is admin)
How it works:
- Raw form data is saved to disk on job submission (excluding passwords)
- Clicking relaunch navigates to the form with
?prefillJobId=<id>parameter - Form loads with all previous values, respecting field dependencies and async queries
- Users can modify values before resubmitting
Make cascaded dropdowns
enum fields (AKA dropdown boxes) can contain placeholders in their query or expression property in the format of $(another_field) or $(another_field[0].name).
The moment the referenced field changes, the referencing field gets re-evaluated, resulting in dynamic and cascading dropdown boxes.
The power of this concept lies in the client web-application that is re-evaluating fields every 100ms. With current processors and the chromium engine, this should be a very seamless experience.
Note: When you reference another enum field, you reference the selected values, NOT the full dropdown list. Use the placeholderColumn-property or a dot-notation like $(city.name)
New in v4.0.20 : setting placeholderColumn to “*” will output 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
Reference another fields value
Placeholders are references to other fields in the forms.
A placeholder is always in the format $(reference). Expressions or queries can contain placeholders.
If the placeholder is pointing to a simple field (text, number, password), it will hold that fields value.
If the placeholder is pointing to a object-based-enum field, then you must either use the placeholderColumn-property or a dot-notation like $(city.name).
If the placeholder is pointing to an expression field, then either the full object is returned or you can have an advanced placeholder reference like $(myarray[0].name) where you can create javascript-like references.
Note: Important to know is that the placeholder is replaced BEFORE the evaluation of the expression. If you expect the result to be a string, then you must wrap it with quotes!. New in v4.0.20 : setting placeholderColumn to “*” will output 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
You can hide a field using the field property hide .
Or you can show/hide a field dynamically using the field properties dependencies and dependencyFn.
Group fields together in a block
Use the field property group . Fields with the same group name will be grouped in a block.
Validate a field
Have a look at the many validation field properties such as regex, minValue, notIn, …
Pass credentials
Credentials can be add in several ways.
- using the field-property
asCredential - using the
credentialsform-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
expression: "'vcenter'"
- name: ad_cred
type: expression
runLocal: true
expression: "'ad'"
# Method 2 : using credentials form-property
name: myplaybook
type: ansible
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
Pass the current jobid
Ansible Forms automatically sends the current jobid in the extravars.
You don’t need to do anything.
It is sent as __jobid__.
Pass the current user
Ansible Forms automatically sends the userinformation in the extravars.
You don’t need to do anything.
It is sent as ansibleforms_user.
Access current user info in the form
The field __user__ is automatically added in the form.
expression: $(__user__)
expression: "'$(__user__.username)'"
expression: $(__user__.groups)
expression: $(__user__.roles)
Customize Ansible Forms
Ansible Forms is a web-app. If you run it natively in nodejs, you could replace files or change them.
But more recommended is to run it as a docker-image and add volume or file mappings. Our docker-compose projects already maps directories to make the database, playbooks, logs, certificates and ssh-keys persistent. Nothing is keeping you from adding more mappings to, for example, override the logo.
There is also a custom.js file where you can add your own javascript functions to use in expressions. Just like you can address our functions with the prefix fn. (fn.fnRestBasic for example) you can access the custom functions with prefix fnc..
And you can add your own jq definitions as well in the same way with the jq.custom.definitions.js
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.js:/app/dist/src/functions/jq.custom.definitions.js
# Map custom sshkey to local node .ssh location
- ./data/ssh:$HOME_DIR/.ssh
- ./data/git/.gitconfig:$HOME_DIR/.gitconfig
# Map custom logo
- ./data/mylogo.svg:/app/dist/views/assets/img/logo_ansible_forms_full_white.svg
Enum default value
There is obviously the field property default you can use to manipulate a default.
And with enum fields, you can use __auto__ for example to automatically select the first item.
But sometimes you want to have a dynamic default, based on an expression.
See the below example how we accomplish this.
# 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
For text, number or date fields, you can use the default property to set a default value.
But what if you want this to be dynamic? Like an expression?
You can do this in 2 ways:
editable: use the editable property to make an expression-field editableevalDefault: 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
Query information from AWX or Ansible Automation Platform
Sometimes you want to create dropdown boxes, with data from AWX or Tower.
You can use fn.fnRestBasic or fn.fnRestJwtSecure to query to do this.
name: Query awx
type: awx
awx: myAwxConfigName
template: my_template # will be overwritten by the field __template__
description: ""
awxCredentials:
- vmware
executionEnvironment: my_execution_environment
roles:
- public
categories: []
inventory: my_inventory # will be overwritten by the field __inventory__
tileClass: has-background-info-light
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: Inventory
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: Inventory
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: Inventory
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
About repositories
Sometimes you want collaboration and versioning and then git repositories are perfect.
In version 5.0.0 you can now manage git repositories.
Every repository is a subfolder of the repositories-path (REPO_PATH environment variable).
Just go to settings / repositories and start managing repositories. You can either add ssh-based repositories (with public key/known hosts) or https based repositories, public or private with username/password/token.
Since version 6.1.0, you can add multiple repositories and use specific switches to control what they’re used for:
Repository Switches (6.1.0+)
- use for config - Repository contains config.yaml (categories, roles, constants)
- use for forms - Repository contains forms (supports multiple repositories, forms are merged)
- use for playbooks - Repository contains Ansible playbooks and roles
- use for vars files - Repository contains vars files for forms
Repository Structure
Each repository can contain subfolders or files directly in the root:
- config.yaml - Placed in repository root (when using “use for config”)
- forms/ subfolder or root - Forms YAML files (when using “use for forms”)
- playbooks/ subfolder or root - Ansible playbooks (when using “use for playbooks”)
- vars/ subfolder or root - Vars files (when using “use for vars files”)
If a subfolder doesn’t exist, AnsibleForms will fall back to the repository root.
Single Repository vs Multiple Repositories
Single repository approach (all in one):
- Enable all switches on one repository
- Structure:
config.yamlin root,forms/,playbooks/, andvars/subfolders
Multiple repository approach (separated):
- Use separate repositories for config, forms, playbooks, and vars files
- Each repository can have files directly in root (no subfolders needed)
- Forms can come from multiple repositories (will be merged)
Important notes:
- Only ONE repository should have “use for config” enabled (warning if multiple)
- Only ONE repository should have “use for playbooks” enabled (playbooks cannot be merged)
- Only ONE repository should have “use for vars files” enabled
- MULTIPLE repositories can have “use for forms” enabled (forms will be merged)
Configuration Priority
Config loading (first match wins):
- Database (if imported)
- Repository with “use for config” enabled
- Repository with “use for forms” enabled (backwards compatibility)
- Local CONFIG_PATH file
- Legacy forms.yaml file
Additional Features
You can choose if the repository must be cloned when AnsibleForms starts, and you can add cron-schedule to schedule recurring pull-actions.
Additionally, in the swagger interface, you will find a clone and pull rest api for webhooks.
In case you want long-lived access tokens for the webhooks, with swagger you can pass an expiryDays parameter (for admin roles only) and create long-lived tokens.
Enable ytt
In the case you want to use ytt, it can be enabled by setting USE_YTT=1.
Read more info about ytt (https://carvel.dev/ytt/).
NOTE : This feature has not been heavily tested and was added as an enhancement with no feedback after it was added.
NOTE : when using ytt, you must disable the designer, the designer will convert the yaml files to intermediate json and will drop the ytt syntax (which is yaml comments).
A lib directory needs to exist within the root directory and is automatically included for 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
Or by providing library data files:
YTT_LIB_DATA_DEMO=/tmp/demo_data.yml
# /tmp/demo_data.yml
message: 'hello demo'
The library demo needs to exists in the ytt context (lib/_ytt_lib/demo/values.yml)
# lib/_ytt_lib/demo/values.yml
#@ data/values
---
demo: {}
Then, the loaded data can 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()