Docs
Workflows
Chain multiple requests together with session variable passing
Workflows
Chain multiple requests together and pass data between them.
Overview
Workflows execute requests in sequence, capturing responses for use in subsequent requests. This is essential for testing multi-step processes like authentication flows, CRUD operations, and complex API interactions.
Key features:
- Sequential request execution
- Session variable capture with
out - Access previous responses via
{{.session.varName}} - Label-based filtering via scenarios
Workflow Structure
Required Fields
| Field | Type | Description |
|---|---|---|
steps | array | Ordered list of requests to execute (required) |
Optional Fields
| Field | Type | Description |
|---|---|---|
description | string | Human-readable description |
labels | array | Labels for filtering workflows via scenarios |
WorkflowStep Fields
| Field | Type | Description |
|---|---|---|
request | string | Name of the request to execute (required) |
out | string | Variable name to capture response in session |
Basic Example
workflows:
auth-flow:
description: "Login and access protected resource"
labels: [e2e, auth]
steps:
- request: login
out: auth # Capture response as "auth"
- request: get-profile
# Access via {{.session.auth.body.token}}
Session Variables
When you capture a response with out: varName, you can access its data in subsequent requests.
Available Session Fields
| Field | Type | Description |
|---|---|---|
{{.session.varName.status}} | number | HTTP status code |
{{.session.varName.headers.HeaderName}} | string | Response header value |
{{.session.varName.body}} | object/array | Parsed JSON response body |
{{.session.varName.body.field}} | any | Specific field from JSON body |
{{.session.varName.body.nested.field}} | any | Nested field access |
{{.session.varName.bodyRaw}} | string | Raw response body as string |
Accessing JSON Body Fields
# Simple field access
url: "/users/{{.session.auth.body.userId}}"
# Nested field access
url: "/orgs/{{.session.auth.body.user.organization.id}}"
# Header access
headers:
Authorization: "Bearer {{.session.auth.body.token}}"
X-Request-ID: "{{.session.prevRequest.headers.X-Request-ID}}"
Accessing Array Elements
Use Go template index function for array access:
# Access first element's field
body:
itemId: "{{(index .session.items.body 0).id}}"
# Access nested array
url: "/items/{{(index .session.data.body.items 0).id}}"
Complete Examples
Authentication Flow
Folder structure:
my-api-tests/
├── playbook.yaml
playbook.yaml:
vars:
default:
baseUrl: "https://api.example.com"
username: "testuser"
password: "testpass"
workflows:
login-and-access:
description: "Complete authentication flow"
labels: [auth, e2e, smoke]
steps:
- request: login
out: auth
- request: get-profile
- request: list-resources
- request: logout
requests:
login:
method: POST
url: "{{.vars.baseUrl}}/auth/login"
body:
username: "{{.vars.username}}"
password: "{{.vars.password}}"
expect:
status: 200
body:
$.token: exists
$.userId: exists
get-profile:
method: GET
url: "{{.vars.baseUrl}}/users/{{.session.auth.body.userId}}"
headers:
Authorization: "Bearer {{.session.auth.body.token}}"
expect:
status: 200
body:
$.id: "{{.session.auth.body.userId}}"
list-resources:
method: GET
url: "{{.vars.baseUrl}}/resources"
headers:
Authorization: "Bearer {{.session.auth.body.token}}"
expect:
status: 200
body:
$: type array
logout:
method: POST
url: "{{.vars.baseUrl}}/auth/logout"
headers:
Authorization: "Bearer {{.session.auth.body.token}}"
expect:
status: 200
scenarios:
auth-tests:
description: "Run authentication workflow"
labelSelector: "auth && e2e"
Run with:
quadrastack --scenario auth-tests
CRUD Workflow
workflows:
user-crud:
description: "Complete user CRUD operations"
labels: [crud, users]
steps:
- request: create-user
out: created
- request: read-user
- request: update-user
- request: verify-update
- request: delete-user
- request: verify-deletion
requests:
create-user:
method: POST
url: "{{.vars.baseUrl}}/users"
body:
name: Test User
email: test@example.com
expect:
status: 201
body:
$.id: exists
read-user:
method: GET
url: "{{.vars.baseUrl}}/users/{{.session.created.body.id}}"
expect:
status: 200
body:
$.name: Test User
update-user:
method: PUT
url: "{{.vars.baseUrl}}/users/{{.session.created.body.id}}"
body:
name: Updated User
email: updated@example.com
expect:
status: 200
verify-update:
method: GET
url: "{{.vars.baseUrl}}/users/{{.session.created.body.id}}"
expect:
status: 200
body:
$.name: Updated User
delete-user:
method: DELETE
url: "{{.vars.baseUrl}}/users/{{.session.created.body.id}}"
expect:
status: 204
verify-deletion:
method: GET
url: "{{.vars.baseUrl}}/users/{{.session.created.body.id}}"
expect:
status: 404
OAuth Token Exchange
workflows:
oauth-flow:
description: "OAuth authorization code flow"
labels: [oauth, auth]
steps:
- request: get-auth-code
out: authCode
- request: exchange-token
out: token
- request: access-resource
requests:
get-auth-code:
method: POST
url: "{{.vars.baseUrl}}/oauth/authorize"
body:
client_id: "{{.vars.clientId}}"
redirect_uri: "{{.vars.redirectUri}}"
response_type: code
expect:
status: 200
body:
$.code: exists
exchange-token:
method: POST
url: "{{.vars.baseUrl}}/oauth/token"
body:
code: "{{.session.authCode.body.code}}"
grant_type: authorization_code
client_id: "{{.vars.clientId}}"
client_secret: "{{.vars.clientSecret}}"
expect:
status: 200
body:
$.access_token: exists
$.token_type: Bearer
access-resource:
method: GET
url: "{{.vars.baseUrl}}/api/protected"
headers:
Authorization: "{{.session.token.body.token_type}} {{.session.token.body.access_token}}"
expect:
status: 200
Two-Factor Authentication
workflows:
2fa-login:
description: "Login with 2FA verification"
labels: [auth, 2fa]
steps:
- request: submit-credentials
out: challenge
- request: submit-2fa-code
out: auth
- request: access-protected
requests:
submit-credentials:
method: POST
url: "{{.vars.baseUrl}}/auth/login"
body:
username: "{{.vars.username}}"
password: "{{.vars.password}}"
expect:
status: 200
body:
$.challengeId: exists
$.requires2FA: true
submit-2fa-code:
method: POST
url: "{{.vars.baseUrl}}/auth/verify"
body:
challengeId: "{{.session.challenge.body.challengeId}}"
code: "{{.vars.totpCode}}"
expect:
status: 200
body:
$.token: exists
access-protected:
method: GET
url: "{{.vars.baseUrl}}/api/protected"
headers:
Authorization: "Bearer {{.session.auth.body.token}}"
expect:
status: 200
Running Workflows
Workflows are executed via scenarios using label selectors.
Define a Scenario for Your Workflow
workflows:
my-workflow:
labels: [e2e, critical]
steps:
- request: step1
- request: step2
scenarios:
run-e2e:
description: "Run E2E workflows"
labelSelector: "e2e"
Run the Scenario
# Run the scenario
quadrastack --scenario run-e2e
# Run with a specific profile
quadrastack --scenario run-e2e --profile staging
Best Practices
1. Use Descriptive Output Names
# Bad
steps:
- request: login
out: r1
- request: get-data
out: r2
# Good
steps:
- request: login
out: auth
- request: get-data
out: userData
2. Validate Each Step
Ensure critical fields exist before using them in subsequent steps:
requests:
login:
method: POST
url: "{{.vars.baseUrl}}/auth/login"
expect:
status: 200
body:
$.token: exists # Validate token exists
$.userId: exists # Validate userId exists
3. Keep Workflows Focused
# Bad: One giant workflow
workflows:
everything:
steps:
# 50 steps...
# Good: Multiple focused workflows
workflows:
auth-flow:
labels: [auth]
steps:
- request: login
- request: verify-session
data-flow:
labels: [data]
steps:
- request: fetch-data
- request: process-data
4. Use Variables for Flexibility
vars:
default:
baseUrl: "http://localhost:8080"
staging:
baseUrl: "https://staging.example.com"
production:
baseUrl: "https://api.example.com"
requests:
login:
method: POST
url: "{{.vars.baseUrl}}/auth/login"
Limitations
| Limitation | Description |
|---|---|
| Workflow-scoped sessions | Session variables are only available within the same workflow execution |
| No cross-workflow sharing | Cannot share session data between different workflows |
| No loops or conditionals | Use multiple workflows for branching logic |
| No persistence | Session data is not persisted between CLI runs |
| Free tier: 5 steps max | Free tier limits workflows to 5 steps |
See Also
- Requests - Define HTTP requests
- Variables - Environment configuration
- Templating - Template syntax reference
- Scenarios - Group and run workflows