Sequences

Sequences let you chain multiple API requests together, capturing data from one request to use in the next.

Basic Sequence

Wrap related requests in a sequence block:

.norn
sequence CreateAndVerifyUser
    POST https://api.example.com/users
    Content-Type: application/json

    {"name": "Alice", "email": "alice@example.com"}

    GET https://api.example.com/users/1
end sequence

Response Capture

Use $1, $2, etc. to reference responses by their order:

.norn
sequence UserWorkflow
    POST https://api.example.com/users
    {"name": "Alice"}
    
    # $1 refers to the POST response above
    var userId = $1.body.id
    
    GET https://api.example.com/users/{{userId}}
    
    # $2 refers to this GET response
    assert $2.status == 200
end sequence

Response Properties

Access different parts of each response:

  • $1.status - HTTP status code
  • $1.body - Response body (parsed as JSON)
  • $1.body.path.to.value - Nested value in body
  • $1.headers.Content-Type - Response header
  • $1.duration - Request duration in milliseconds
  • $1.cookies - Cookies captured from the response

Test Sequences

Mark sequences as tests with the test keyword. Test sequences run in the Test Explorer and CLI:

.norn
test sequence UserAPITests
    GET https://api.example.com/users
    assert $1.status == 200
    assert $1.body[0].id exists
end sequence

Sequence Composition

Sequences can call other sequences with run so you can separate setup, shared flows, and cleanup.

.norn
sequence Login
    POST {{baseUrl}}/auth/login
    Content-Type: application/json

    {"username": "admin", "password": "secret"}

    var token = $1.body.accessToken
    return token
end sequence

test sequence UserFlow
    var token = run Login
    GET {{baseUrl}}/users/me
    Authorization: Bearer {{token}}
    assert $1.status == 200
end sequence

Sequence Parameters and Returns

Sequences can accept parameters. A single returned value comes back as the raw value; multiple return values come back as an object keyed by name.

.norn
sequence GetTodoTitle(id)
    GET {{baseUrl}}/todos/{{id}}
    var title = $1.body.title
    return title
end sequence

sequence AuthenticateAndFetch
    POST {{baseUrl}}/login
    {"user": "demo"}
    var sessionId = $1.body.sessionId

    GET {{baseUrl}}/todos/1
    var todoTitle = $2.body.title

    return sessionId, todoTitle
end sequence

test sequence ReturnsExample
    var title = run GetTodoTitle(1)
    var auth = run AuthenticateAndFetch

    assert title isType string
    assert auth.sessionId exists
    assert auth.todoTitle isType string
end sequence

Named Requests

Named requests let you define a reusable request block once and invoke it with run.

.norn
[FetchTodo]
GET {{baseUrl}}/todos/1

[FetchUser]
GET {{baseUrl}}/users/1

test sequence ReuseRequests
    run FetchTodo
    assert $1.status == 200

    var user = run FetchUser
    assert user.status == 200
end sequence

Sequence Tags

Add tags to organize and filter tests:

.norn
@smoke
@team(backend)
test sequence CriticalPathTest
    GET https://api.example.com/health
    assert $1.status == 200
end sequence

Run tagged tests from CLI: norn tests/ --tag smoke

Parameterized Tests

Use @data for data-driven tests:

.norn
@data(1, "Alice")
@data(2, "Bob")
test sequence UserTest(id, expectedName)
    GET https://api.example.com/users/{{id}}
    assert $1.body.name == "{{expectedName}}"
end sequence

For external test data, use @theory with a JSON array of objects whose keys match the sequence parameters.

.norn
@theory("./testdata/users.json")
test sequence UserTheory(userId, expectedId)
    GET {{baseUrl}}/users/{{userId}}
    assert $1.status == 200
    assert $1.body.id == {{expectedId}}
end sequence
testdata/users.json
[
  { "userId": 1, "expectedId": 1 },
  { "userId": 2, "expectedId": 2 }
]