As described earlier, a playbook is a YAML list of one or more Play structures. It must be a list, you cannot have a file that has a playbook loose by itself, but a list of just one play is fine.
Plays assign work (or configurations) to Groups from Inventory in the form of Tasks and Roles.
This is all best understood while looking at our example repo.
What's In A Name?
A common phrase in IT is often the word "runbook", here playbook is meant - as Michael originally used it in Ansible - as a kind of sports analogy, there are multiple different plays a team could run for different game-time situations. In the end, it's still the same concept, just meant to be slightly more fun. Beyond this, Jet mostly rejects metaphors, themes, and such, which is why you won't see anything called "landing gear" or "fuselage" or anything like that. Ansible is a registered Trademark of Red Hat Inc.

Playbook Syntax

A basic playbook file might looks like the file below.
The following example selects two groups from inventory (in the case of SSH) and applies two different tasks to any host machine that is included in either group.
When Is Inventory Needed?
Local configuration mode does not use inventory, local playbooks should be written targetting the 'all' group. Planetary Scale features will also not need inventory.
# playbook1.yml
# not using roles
- name: sample playbook
- groupA
- groupB
- !shell:
cmd: echo hi
- !dnf
package: tree
Task sections are full of invocations of Modules. Playbooks can contain loose tasks, tasks loaded in from roles, or both.
As mentioned before, most users should consider organizing tasks into Roles to better manage their configuration policies as they grow and encompass more and more projects or services.
Roles Vs Loose Tasks: What Executes First?
If loose tasks and roles exist in the same playbook, roles will be executed first.
Having a playbook with no loose tasks (one that just uses roles) can also be much cleaner to look at, so you may find yourself preferring this approach once learning the program:
# playbook2.yml
# just applying some roles
- name: roles demo
- groupA
- groupB
- role: common
- { role: example_app_two, vars: { port: 3005, sword: 1, battlecat: 1 } }
Each play runs in order, and a total failure of all hosts in one play will stop the execution of the playbook.
# playbook3.yml
# deploy a multi-tier application using roles
- name: caching tier
- caching
- name: common
- name: caching_layer
- name: security
- name: database tier
- database
- role: common
- role: database_setup
- role: security
- name: webserver tier
- webservers
- role: common
- role: webservers
- role: security
When a set of playbooks completes (or if it fails), Jet will output a nice summary of statistics about the playbook run.
Ok, so we're about done going over the play structure!
There are other optional keywords that can be used in plays, including the ability to declare variables either directly in the playbook or using YAML files on disk. The following example shows everything we can set at play level. Below is an arbitrary example of are all the playbook keywords you can use, though having to use them all together would be somewhat unusual:
# playbook3.yml
# all the options together in a very unrealistic example
- name: too many options, why would you do this
groups: [ 'groupA', 'groupB' ]
ssh_user: opsteam
ssh_port: 22
# variable section
some_value: 1000
other_value: 8675309
- vars.yml
- { role: alpha, tags: [ 'alpha' ] }
- { role: beta, tags: [ 'beta' ] }
- { role: gamma, vars: { x: 1234, y: 'asdf' }, tags: ['gamma'] }
- !template
src: foo.conf.hb
dest: /etc/foo/foo.conf
notify: restart foo
- !sd_service
service: foo
restart: true
subscribe: restart foo
The keywords in a play, shown above, are:


Sets default variables at a very low level of precedence that are easily overriden by both inventory variables and role parameters.


Specifies what groups of hosts to communicate with. Unlike the other parameters on a play, this parameter is required. For local playbooks, the group name should be set to 'all'.
Group names must be matched exactly for them to be targetted. Specifying the name of a group that is not in inventory will intentionally cause an error.


Handlers are tasks that only run when certain resources are notified, and are mainly used for restarting stateful services (such as systemd services). This is described in a more detail in the Handlers chapter.


The playbook must have a name which gives an explanation of what the play is about to do. This is shown in the output and helps show what is going on when multiple plays exist in the same playbook


if specified, adds roles to apply to the groups. See the CLI Instructions for how to configure the roles search path.
A 'roles/' directory parallel to the playbook file is automatically included in the roles search path.
- name: demo of role inclusion
- all
- { role: example }
- { role: example2, vars: { x: 5000, y: "foo" } }
- { role: example3, tags: ['example3'] }
# ...
The 'vars' keyword inside the role invocation allows roles to be parameterized. This means a role could be developed for common usage in many different playbooks, and used slightly differently in each playbook depending on the vars passed to it. For instance, a role may represent a customer installation of a particular piece of software (passing in a customer ID).
Roles can also be tagged like tasks, in which case the given tag is applied to all tasks within the role. See Tasks for more information on the tag feature and how to use it.


If --sudo username is not specified on the command line, this playbook setting will cause all tasks in the play to sudo to the given username unless a "sudo" override is set on a specific task to pick a different user.
- name: demo of sudo config
- all
sudo: root
- ...
Dealing With Sudo Passwords
to use the sudo feature, sudoers rights on the remote system should currently be configured to allow NOPASSWD access on any automation accounts as the sudo command does not support interactive password prompts at this time. Support for sudo passwords will be added in the near future. Since sudo passwords are transmitted to remote clients, they don't really represent a strong security feature, but we recognize many users have these enabled per organizational policies.
Sudo Refinement Options
Because jet works via the shell, it is still possible to finely control to restrict what specific shell commands a user can run via jet over sudo via /etc/sudoers, but this is somewhat misleading, once package installation (apt, dnf, etc) is allowed, these commands can do basically anything. Providing sudoers access to run all commands is a suggestion, but either method works as long as the limitations are understood.


By default Jet will use 'sudo' to interpret 'sudo' directives, but other similar commands for alternative sudo tools are supported! The optional parameter sudo_template on a playbook configures this behavior:
- name: showing sudo template syntax
- databases
- webservers
sudo: root
sudo_template: "/usr/bin/sudo -u '{{jet_sudo_user}}' {{jet_command}}"
# ...ssh_user / ssh_port
It is important to note these variables in the sudo template line are not "normal" variables in jet, and variables other than jet_sudo_user and jet_command cannot be used inside sudo_template keyword value. Because these are not really normal variables, it is also intentionally impossible to programmatically set these variables anywhere in the program, including inventory.
Sudo Replacements and Passwords
As per the above section explaining sudo, responding to a sudo-like command with a password with these templates is not currently supported but may be implemented later.
When using sudo_template, be sure to fully path the address of the setuid program to ensure you are executing the right program.

ssh_user / ssh_port

For SSH-based deployment cases, connections default to the current logged in username on the control machine and port 22.
The username can be overriden by --user on the command line, and both the user and port by specific parameters in inventory variables, as described in SSH.
For situations where all hosts share a common user and/or port, a value for all hosts can also be specified in the playbook. For instance, this might be used if all machines used port 5222 for SSH, or all users logged in with the name "opsteam". Both of these parameters are optional.
- name: showing ssh user/port global config
- all
ssh_user: opsteam
ssh_port: 5222
# ...


If specified, lists any free tasks that are not defined in roles. Tasks are fundamental to doing anything with Jet, so consult the example content if you do not understand them after reading the task section and module documentation.
Users will usually start learning Jet with tasks and progress to roles, but it's fine for one-off and simple playbooks to use tasks without adopting roles. Loose tasks are not necessarily a mark of a "wrong" playbook, but rather one that did not need to have content reused elsewhere.


Sets some additional variable values that can be used in playbooks and templates, that will override any other variable definition.
While it is up to you, we suggest using this feature very sparingly and using defaults instead, which allows inventory variables to have some override ability. Use vars (or vars_files, just below) for variables that must be used as entered.


This works like vars but loads variables from YAML dictionaries according to the specified paths.
The paths are relative to the playbook root for relative paths. The files should define YAML dictionaries (also known as hash tables or maps). Any errors in YAML syntax will be noted during playbook evaluation.