Writing a Spec
Manual tests, known as specs, are written in Markdown (CommonMark, with GitHub-style tables, task lists, autolinks and strikethrough), enhanced with the Liquid template language. This page covers the spec format itself, which is the same whether your project is Standalone or Connected.
See Creating & Editing Specs for how to write, preview, and save a spec, including the Standalone editor's toolbar.
Test Syntax
A spec is a single Markdown document consisting of:
- A Front Matter
header block - A test
specname - At least one test
case - Optional test
steps
---
testspace:
---
# Test 1
## Case 1
- do this
- do that
## Case 2
..
See the Markdown Cheat Sheet for helpful hints on leveraging markdown to capture test instructions.
Front Matter
Every spec starts with a metadata block called front matter: YAML between two triple-dash (---) lines, containing the testspace: identifier. It also holds the spec's custom variables and its automated fixtures.
---
testspace:
---
- Connected: the front matter is the first thing in the
.mdfile, and identifies the file as a Testspace spec. - Standalone: the editor adds the front matter when you submit, so you don't type it. Custom variables and fixtures go in the editor's Variables box, or through Manage (see Creating & Editing Specs). A Standalone spec can't be saved without the identifier ("Spec must include the testspace front matter identifier").
Disable Spec
To disable a spec, set the testspace value to false:
---
testspace: false
---
A disabled spec can't be selected for a new session or run ("Disabled - running is disallowed."). It stays in the Space, so its existing status, and any failures being tracked for it, are kept.
Remove Spec
In a Connected project, to remove a spec without deleting its file, comment out (#) the testspace identifier:
---
#testspace:
---
Testspace then treats the spec as deleted: its status is removed from the next Result, and its tracked failures are retired at the next session (Failure Tracking). In a Standalone project, use Delete on the spec instead (Creating & Editing Specs).
Spec
Each spec requires a spec name written as a Markdown <H1>, following the front matter. Any text after the spec name is the spec's description. A Standalone spec with no <H1> can't be saved ("Spec content can't be empty -- give it a title.").
---
testspace:
---
# My Spec
Spec description text
Case
A spec requires at least one test case. A case name is a Markdown <H2> heading. Any text following the heading is the case's description.
# My Spec
## My Case
Case description text
## My Other Case
Case names must be unique within a spec. A Standalone spec with two cases of the same name can't be saved ("Duplicate case name is not allowed"); a Connected spec with duplicates fails to parse and can't be selected for a session.
Testspace matches a case to its history by name. Renaming or removing a case loses that case's history, and its tracked failures are retired at the next session that includes the spec (Failure Tracking).
Step
Steps are optional instructions to be executed for a test case. Steps are written using a markdown (ordered or unordered) list inside a test case.
## My Case
* My step 1 instructions
* My step 2 instructions
Steps can also exist outside a test case, in which case they are meant to represent a repeated context that is applicable to each test case in the spec.
Checklist Steps
A step written as a GitHub-style task-list item becomes a checkbox a tester can check off individually while executing the case:
## My Case
- [ ] My step 1 instructions
- [ ] My step 2 instructions
Fixtures
Test case Fixtures implement steps/conditions that are required for executing each case within the spec. They are defined using the same markdown syntax but named in a special way.
:::info This is a different concept from **Automated Fixtures** (serverless automation that runs before/after a spec) — see [Automated Fixtures](automated-fixtures). :::A fixture can be defined anywhere in the file, but only one time.
Setup is a fixture, defined as a set of instructions, to be presented before each case during execution.
A Setup fixture is defined using the ## [setup] syntax:
## [setup]
This is my setup fixture:
- First do this
- Next do that
The Setup fixture can be defined anywhere in the file, but only one time.
Teardown is a fixture, defined as a set of instructions, to be presented after each case during execution.
A Teardown fixture is defined using the ## [teardown]:
## [teardown]
This is my teardown fixture:
- Now do this
- And do that
Template Language
Testspace supports the template language called Liquid. Specs are handled as templates, meaning they get processed before being rendered. A spec can use variables, include files (i.e. subroutines) with parameters, and conditional logic.
In a Standalone Project, the editor's toolbar inserts most of this for you: Include, Variable and If, plus the presentational tags. See Creating & Editing Specs.
Variables
There are two types of variables supported by Testspace: custom and global.
Custom
User-defined "custom" variables are defined in the front matter block and can be referenced throughout the spec. In a Standalone Project, add them with Manage or type them in the editor's Variables box. id, filename and filepath are reserved names.
---
testspace:
var1: Hey there
---
When referencing a variable, the following syntax is required: {{ spec.VAR }}.
## My Case
* My first step uses this {{ spec.var1 }}
* My Other Step
Scalar custom variable values are always stored and interpolated as strings, even something that looks numeric — count: 5 becomes the string "5", not an integer. Comparing with ==/>/< against an unquoted numeric literal silently never matches: {% if spec.count == 2 %} is never true, since spec.count is the string "2", not the number 2. Quote the literal instead: {% if spec.count == "2" %}.
A range, e.g. {% for i in (1..spec.count) %}, isn't affected — Liquid converts both bounds to integers regardless of the input's type.
Global
The following global variables are supported: {{ variable }}.
| Variable | Description |
|---|---|
project.name | The name of the Testspace project |
project.id | The assigned project id |
space.name | The name of the space |
space.id | The assigned space id |
spec.filename | The spec's file name, excluding the folder (e.g. login-steps.md) |
spec.filepath | The spec's folder (e.g. specs in Connected; . for a Standalone spec with no folder) |
spec.id | The spec's id (in an automated fixture's context, spec.id is the id of the spec's run instead) |
repo.branch | The branch name (same as the space.name). Connected Projects only — always empty for Standalone. |
repo.url | The branch URL (e.g. "https://github.com/org/repo"). Connected Projects only — always empty for Standalone. |
The following global variables take their values only when the spec is run in a session. In the editor's Preview, session.* shows as a placeholder:
| Variable | Description |
|---|---|
user.name | The name of the user running the spec |
user.id | The id of the user running the spec |
session.name | The session's name |
session.id | The session's id |
The following is a simple example of referencing a global variable:
## My Case
Enter the session name: {{ session.name }} in the comments below
* Check this
* Check that
Includes
The include tag pulls in content from another reusable file, referenced by name:
{% include file.md %}
Both custom and global variables are referenceable within an include file using the {{ }} syntax. Includes can reference other includes.
Avoid circular references — an include that, directly or indirectly, ends up including itself will cause the spec to fail to parse.
# My Spec
* Step One
{% include morestuff.md %}
{% include sub-folder/otherstuff.md %}
Where an include's content actually lives, and how you create and manage one, depends on your project type — see Includes.
Parameters
Parameters can also be passed to the included file using var="string":
{% include morestuff.md domain="testspace.com" %}
When referencing a passed "parameter" via the include tag, the following syntax is required: {{ include.VAR }}.
Logic
Logic and control flow for specs is accomplished using Liquid tags. The curly-brace percentage delimiters {% %} and the text that they surround, does not produce any visible output when the spec is rendered. The tags enable assigning variables and creating conditions and loops without showing any of the Liquid logic on the page.
if-else (assign sets a variable for this example — see Variables in the Liquid Cheat Sheet below)
{% assign OS = 'Windows' %}
{% if OS == 'Windows' %}
- check for Windows stuff.
{% elsif OS == 'Linux' %}
- check for Linux stuff
{% else %}
- check for Mac stuff
{% endif %}
for-loop
{% for i in (1..5) %}
{{ i | plus: 100}}
{% endfor %}
Presentational Tags
A small set of Testspace-specific Liquid tags render as formatted UI elements — callouts, tabs, and a collapsible section — rather than plain markdown. Available on both Connected and Standalone Projects (they don't render in Desktop Preview). Each compiles to a fixed, server-rendered fragment — same trust model as {% include %} — nothing a spec author writes here gets executed as code.
- Write
- Preview
In a Standalone Project, each has a button in the spec and include editor's toolbar (Tabs, Note, Warning, Collapse) that inserts a starting skeleton for it.
The four tags, at a glance
{% note %} … {% endnote %}
Context a tester needs before starting — preconditions, environment assumptions, seed data.
{% warning %} … {% endwarning %}
Flags a known gotcha inline, at the exact step it applies to — flaky data, environment quirks, "don't file a bug for this."
{% tabs %} {% tab "…" %}
One case, several platform- or role-specific step lists — instead of duplicating the whole spec, or one giant if/else wall.
{% collapse "…" %} … {% endcollapse %}
Reference material worth keeping next to the steps — a payload, a query, a longer explanation — without pushing past it to keep reading.
Markdown inside these tags — including task-list steps (- [ ] ...) — renders normally, same as anywhere else in a spec.
Examples
The following examples show more complete specs combining variables, includes, parameters, and logic.
Example using an include
## Case 2
My description here
- Check this subdomain: {{ spec.subdomain }}
- Click here: https://{{ spec.subdomain }}.{{ include.domain }}
Example of a spec including an include and passing a parameter:
---
testspace: true
title: Vars and Include
subdomain: s2
---
# {{ spec.title }}
Some description
## Case 1
- Do this
- Do that
{% include inc.md domain="testspace.com" %}
Renders to:
Vars and Include
Some description
Case 1
- Do this
- Do that
Case 2 My description here
- Check this subdomain: s2
- Click here: https://s2.testspace.com
Example using data driven testing
Using front matter data to drive testing:
---
testspace:
title: OS Systems
matrix: # test different OS systems
- name: Windows
timeout: 27 seconds
reqs: "[Windows details](https://staging7.newco.com/windows)"
- name: Linux
timeout: 14 seconds
reqs: "[Linux details](https://staging7.newco.com/linus)"
---
# {{ spec.title }}
Matrix being tested.
Name | Timeout | Info
-----| --------| -----
{%- for os in spec.matrix %}
{{ os.name }} | {{ os.timeout}} | {{os.reqs -}}
{% endfor %}
{% for os in spec.matrix %}
## Test {{ os.name }}
* check for correct timeout: {{ os.timeout}}
* check on requirements: {{ os.reqs }}
{% endfor %}
Renders to:
OS Systems
Matrix being tested.
| Name | Timeout | Info |
|---|---|---|
| Windows | 27 seconds | Windows details |
| Linux | 14 seconds | Linux details |
Test Windows
- check for correct timeout: 27 seconds
- check on requirements: Windows details
Test Linux
- check for correct timeout: 14 seconds
- check on requirements: Linux details
Example combining presentational tags with data-driven content
Presentational Tags aren't a separate, static feature — they compose with everything else on this page. Here, a {% collapse %} and forloop.index (see Loops in the Cheat Sheet below) are generated fresh for each row of front matter data:
- Write
- Preview
Markdown Cheat Sheet
The following Markdown syntax is supported.
Refer here - https://commonmark.org/help/
- Lists
- Emphasis
- Tables
- Images
- Code block
- Links
- Quoting text
- Emoji
- Comments
Lists
Lists are used within steps and fixtures, but can also be nested.
Bullets
- First Step
- sub-step 1
- sub-step 2
- Second Step
* First Step
* sub-step 1
* sub-step 2
* Second Step
Numbers
- First Step
- Second Step
1. First Step
2. Second Step
Todos
A step written as a task-list item becomes an individually checkable box during execution:
- First Step
- Second Step
- [ ] First Step
- [ ] Second Step
See Checklist Steps for more on task-list steps.
Emphasis
The following emphasis applied to text:
- bold
- Italic
strikethrough- Bold and italic
**bold**
*Italic*
~~strikethrough~~
**Bold** and _italic_
Tables
Tables for rendering a matrix.
| First Header | Second Header |
|---|---|
| Content from cell 1 | Content from cell 2 |
| Content in the first column | Content in the second column |
First Header | Second Header
------------ | -------------
Content from cell 1 | Content from cell 2
Content in the first column | Content in the second column
And an alternative format
| First Header | Second Header|
|------------ | -------------|
| Content from cell 1 | Content from cell 2|
| Content in the first column | Content in the second column|
Images
Images are supported and are displayed during test execution.

On a Standalone project, drag and drop or paste an image into the spec or include editor: it's uploaded and the Markdown reference is inserted for you. On a Connected project, commit the image to the repo and reference it by path (below).
An image can also be referenced directly by URL, on either project type:

For a Connected Project, an image committed to the repo can be referenced by an absolute or relative path:


Code block
Using code blocks requires surrounding the code with ```:
$ testspace results.xml
Inline code also supported:
Example - I think you should use an
<addr> element here instead.
I think you should use an
`<addr>` element here instead.
Links
Link support:
https://testspace.com Testspace
https://testspace.com - automatic!
[Testspace](https://testspace.com)
To add a comment to your spec file that will not be rendered, use the following syntax:
[your comment]: # (content being removed)
If you don't need comment text, use [//] instead:
[//]: # (content being removed)
https://stackoverflow.com/questions/4823468/comments-in-markdown
Liquid Cheat Sheet
Quick reference for Liquid mechanics not already covered above. For variables, includes, and if/for logic, see the Template Language sections above.
Refer here - https://shopify.github.io/liquid/
- Variables
- Control
- Loops
- Filters
- Comments
- Raw
- White space
Variables
Tags for creating variables mid-template, as opposed to reading ones already supplied — see Variables above for those. Every example below is self-contained; paste it as-is into a spec to see it work.
assign — creates or overwrites a variable with a computed value:
{% assign timeout = 5 | times: 2 %}
Timeout is {{ timeout }} seconds
capture — grabs a block's rendered output into a variable instead of displaying it, useful for building up reusable text:
{% assign name = 'Alex' %}
{% capture greeting %}Hello, {{ name }}!{% endcapture %}
{{ greeting }}
increment / decrement — each maintains its own independent counter, separate from any variable of the same name created with assign. increment starts at 0 and outputs the value before incrementing; decrement starts at -1 and outputs the value after decrementing:
{% increment counter %}
{% increment counter %}
{% decrement counter %}
Outputs 0, then 1, then -1.
Full tag reference: https://shopify.github.io/liquid/tags/variable/
Control
Beyond if/else (see Logic above), Liquid supports a couple more branching constructs. Every example below is self-contained; paste it as-is into a spec to see it work.
unless — the inverse of if, reads more naturally for a single negative condition:
{% assign OS = 'Windows' %}
{% unless OS == 'Windows' %}
- skip the Windows-only steps
{% endunless %}
case / when — cleaner than a long if/elsif chain when checking one value against several possibilities:
{% assign OS = 'Windows' %}
{% case OS %}
{% when 'Windows' %}
- check for Windows stuff.
{% when 'Linux' %}
- check for Linux stuff
{% else %}
- check for Mac stuff
{% endcase %}
Full tag reference: https://shopify.github.io/liquid/tags/control-flow/
Loops
for itself is covered in Logic above — these are the modifiers that control which/how many items a loop visits. limit, offset, and reversed shape the loop up front; break/continue control it from inside. Every example below is self-contained; paste it as-is into a spec to see it work.
{% assign systems = 'Windows,Linux,Mac' | split: ',' %}
{% for os in systems limit: 2 %}
{{ os }}
{% endfor %}
{% assign systems = 'Windows,Linux,Mac' | split: ',' %}
{% for os in systems reversed %}
{% if os == 'Linux' %}{% continue %}{% endif %}
{{ os }}
{% endfor %}
forloop — inside a for block, this holds the loop's own position, handy for numbering generated steps/cases or special-casing the first/last item:
{% assign systems = 'Windows,Linux,Mac' | split: ',' %}
{% for os in systems %}
{{ forloop.index }}. {{ os }}{% if forloop.first %} (primary target){% endif %}{% if forloop.last %} (last one){% endif %}
{% endfor %}
forloop.index is 1-based (forloop.index0 is the 0-based version); forloop.first/forloop.last are true only on those iterations; forloop.length is the total item count.
Full tag reference: https://shopify.github.io/liquid/tags/iteration/
Filters
A filter transforms a value before it's output, using the pipe | syntax. Filters can be chained.
{{ 5 | plus: 100 }}
{{ "hello" | upcase }}
{{ "hello" | downcase | strip }}
Filters useful in specs:
| Filter | Description | Example |
|---|---|---|
plus / minus | Add or subtract a number | {{ 5 | plus: 100 }} |
times / divided_by | Multiply or divide | {{ 10 | times: 2 }} |
upcase / downcase | Change case | {{ "hello" | upcase }} |
capitalize | Capitalize the first word | {{ "hello there" | capitalize }} |
strip | Remove leading/trailing whitespace | {{ " hi " | strip }} |
default | Fall back when a value is blank | {{ "" | default: "unassigned" }} |
size | Length of a string or array | {{ "hello" | size }} |
date | Format a date | {{ "2026-01-01" | date: "%b %d, %Y" }} |
split | Turn a delimited string into an array | {{ "a,b,c" | split: "," }} |
join | Turn an array back into a string, joined by a separator | {{ "a,b,c" | split: "," | join: "; " }} |
Full filter reference: https://shopify.github.io/liquid/filters/
In Liquid, you can include a hyphen in your tag syntax {{-, -}}, {%-, and -%} to strip whitespace from the left or right side of a rendered tag.
{% assign x = 5 %}
{%- if x > 0 -%}
positive
{%- endif -%}
Without the hyphens, the blank lines around {% if %}/{% endif %} would render too.
{% assign xyz = "hey" %}
{% comment %}
{% assign xyz = "NO" %}
{% endcomment %}
{% raw %}
steps:
- uses: testspace-com/setup-testspace@v1
with:
domain: ${{ github.repository_owner }}
token: ${{ secrets.TESTSPACE_TOKEN }} # optional, only required for private repos
...
{% endraw %}