SpecFormula AI
ISABackendInstructions

ApiCall

Execute HTTP API requests with declarative syntax

ApiCall lets you declare API call parameters and response extraction directly in Gherkin, without writing HTTP request code. It supports both DataTable and JSON formats.

Two Formats

FormatUse Case
DataTableFlat fields, intuitive and readable
DocString JSONNested structures, closer to actual Request Body

DataTable Format

When (UID="$Alice.id") create todo, call table:
  | >todo1.id | title    |
  | <todoId   | Buy milk |

JSON Format

When (UID="$Alice.id") create todo, call JSON:
  """json
  {
    >todo1.id: <todoId,
    "title": "Buy milk"
  }
  """

Both produce the same result:

  1. Use $Alice.id as the caller identity (Bearer Token)
  2. Call the API with summary "create todo" in the API Spec
  3. Send title: Buy milk as the Request Body
  4. Extract the todoId field from the response and store it in variable todo1.id

JSON format note: Symbol system expressions ($ variables, & CAS constraints, @ time symbols, >/< extraction keys) must NOT be placed inside JSON string quotes "", otherwise they are treated as plain strings.

{ "title": $todo1.title }    // ✅ Correct: symbol outside quotes
{ "title": "$todo1.title" }  // ❌ Wrong: symbol wrapped in quotes, treated as plain string

Field Syntax

DataTable field names support special prefixes to specify parameter types:

PrefixPurposeExample
(none)Request Body field; if the field name also exists as a Path/Query/Header parameter in the API Spec, it is automatically routed to the matching position (syntax sugar, equivalent to also writing P:/Q:/H:)title, todoId
P:Path parameterP:todoId → replaces {todoId} in URL
Q:Query parameterQ:page → ?page=value
H:Request HeaderH:X-Trace-Id
B:Explicitly mark entire Request Body (bare value, array, or object)B: → cell value becomes entire body
>Store into variable — defines the variable name to save the response value>todo1.id
<Extract from response — specifies the source field in the response JSON<todoId
<H:Extract from response Header — specifies the source header field<H:Location
<B:Extract from response Body (bare value) — extracts entire response body<B:

JSON Format Prefix Syntax

JSON format uses uppercase letter prefixes with quotes, corresponding to the DataTable P:/Q:/H: prefixes:

DataTableJSONDescription
P:todoIdP"todoId"Path parameter
Q:pageQ"page"Query parameter
H:X-Trace-IdH"X-Trace-Id"Request Header
B:B:Explicitly mark entire Body
# DataTable version
When (UID="$Alice.id") update todo, call table:
  | P:todoId  | Q:verbose | H:X-Trace | title        |
  | $todo1.id | true      | trace-123 | Buy soy milk |

# JSON version
When (UID="$Alice.id") update todo, call JSON:
  """json
  {
    P"todoId": $todo1.id,
    Q"verbose": "true",
    H"X-Trace": "trace-123",
    "title": "Buy soy milk"
  }
  """

Variable Extraction Pairing

> and < must be paired vertically in the same column: the header holds the variable name (>), and the row holds the source field (<).

| >todo1.id | >todo1.title | title    |
| <todoId   | <title       | Buy milk |

After execution:

  • todo1.id = value of todoId from response
  • todo1.title = value of title from response

Response Header Extraction

Use the <H: prefix to extract HTTP Header values from the response:

# DataTable — extract Location header
When (No Actor) create resource, call table:
  | >loc        | name          |
  | <H:Location | test resource |

# JSON — extract X-Request-Id header
When (No Actor) create resource, call JSON:
  """json
  {
    >requestId: <H"X-Request-Id",
    "name": "test resource"
  }
  """

Bare Value Response Body Extraction

When API returns bare values (e.g., true, 42) instead of JSON objects, use <B: to extract the entire response body:

When (No Actor) get resource status, call table:
  | >result | P:id  |
  | <B:     | $res1 |

Mapping to API Spec

The ApiCall instruction identifies the API to call using the summary field. This design allows test files to use business language rather than technical URL paths.

Mapping Mechanism

# api-spec.yml (OpenAPI 3.0)
paths:
  /todos:
    post:
      summary: create todo    # ← ApiCall identifies by this summary
      operationId: createTodo
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - title
              properties:
                title:
                  type: string
                priority:
                  type: integer
      responses:
        "201":
          content:
            application/json:
              schema:
                type: object
                properties:
                  todoId:
                    type: integer
                  title:
                    type: string
# Corresponding Gherkin syntax
When (UID="$Alice.id") create todo, call table:
  | title    | priority |
  | Buy milk | 1        |

Mapping Table

Gherkin ElementAPI Spec Source
create todopaths.*.*.summary
HTTP MethodInferred from path definition (post, get, put, delete)
URL Pathpaths key (e.g., /todos)
Request Body fieldsrequestBody.content.*.schema.properties
Path parametersparameters[].in: path
Query parametersparameters[].in: query
Response fieldsresponses.*.content.*.schema.properties

Summary Uniqueness Requirement

Each API operation's summary must be unique across the entire API Spec:

# ✅ Correct: summaries are distinct
/todos:
  post:
    summary: create todo
  get:
    summary: list todos

/todos/{id}:
  get:
    summary: get todo
  put:
    summary: update todo
  delete:
    summary: delete todo

# ❌ Wrong: duplicate summaries cause identification failure
/todos:
  get:
    summary: get todo
/todos/{id}:
  get:
    summary: get todo    # Duplicate!

Automatic Parameter Classification

When a Body field name matches a Path/Query/Header parameter name in the API Spec, the value is sent as a Body field and also automatically routed to the matching parameter position, without requiring a P: or Q: prefix:

# todoId is both Body and Path parameter → automatically routes to P:todoId
| todoId | title        |
| 123    | Buy soy milk |

Nested JSON Construction

Supports dot notation and array indexing for automatic deep JSON construction:

# DataTable — use dot notation for nested structures
| name       | config.theme | tags[0] | tags[1] |
| My Project | dark         | work    | urgent  |

JSON format allows directly writing nested structures, which is more intuitive:

# JSON — describe nested structure directly
"""json
{
  "name": "My Project",
  "config": { "theme": "dark" },
  "tags": ["work", "urgent"]
}
"""

Both produce the same Request Body:

{
  "name": "My Project",
  "config": { "theme": "dark" },
  "tags": ["work", "urgent"]
}

Escaping Keys Containing .

When a JSON key itself contains a . character (e.g., "btn.save"), use ["..."] bracket notation to prevent it from being split into nested levels:

# DataTable — use bracket notation
| content.["btn.save"] | content.["btn.cancel"] |
| Save                 | Cancel                 |

JSON DocString handles escaping automatically during flattening — users write the original key directly:

# JSON — keys naturally contain ".", framework handles escaping
"""json
{
  "content": {
    "btn.save": "Save",
    "btn.cancel": "Cancel"
  }
}
"""

["..."] (string key escaping) and [0] (array indexing) can coexist in the same path:

data.[0].["field.name"]
→ data is an object key, [0] is an array index, ["field.name"] is an object key containing "."

Top-Level Array Body

When the API request body is a top-level array (e.g., batch creation), JSON DocString can use [...] directly as the top level:

When (No Actor) batch create todos, call JSON:
  """json
  [
    { "title": "Buy milk", "priority": 1 },
    { "title": "Write report", "priority": 2 }
  ]
  """

When Path/Query/Header parameters are also needed, keep {} as top level and use B: to mark the body content:

When (No Actor) batch create todos, call JSON:
  """json
  {
    P"projectId": $project1.id,
    Q"notify": "true",
    B: [
      { "title": "Buy milk", "priority": 1 },
      { "title": "Write report", "priority": 2 }
    ]
  }
  """

Equivalent DataTable:

| P:projectId  | Q:notify | [0].title  | [0].priority | [1].title    | [1].priority |
| $project1.id | true     | Buy milk   | 1            | Write report | 2            |

B: also works for object bodies, providing visual separation between parameters and body (the implicit body rule for non-prefixed keys remains backward-compatible):

# Implicit body (current syntax, backward-compatible)
"""json
{ P"projectId": $project1.id, "title": "Buy milk", "priority": 1 }
"""

# Explicit B: body (equivalent)
"""json
{
  P"projectId": $project1.id,
  B: { "title": "Buy milk", "priority": 1 }
}
"""

Note: B: and non-prefixed keys (e.g., "name") cannot coexist — when using B:, the entire body must be inside B:.

Bare Value Body

When the API request body accepts primitive types (boolean, string, integer), use B: to mark bare values:

# DataTable — B: prefix carries bare value
When toggle resource status, call table:
  | P:id  | B:   |
  | $res1 | true |

# JSON — B: carries bare value
When (No Actor) toggle resource status, call JSON:
  """json
  { P"id": $res1, B: true }
  """

When (No Actor) submit feedback, call JSON:
  """json
  { B: "This is feedback" }
  """

Caller Identity

(UID="...") specifies the caller identity, and the framework automatically injects the corresponding Bearer Token:

# Use variable as caller
When (UID="$Alice.id") list todos, call table:
  | Q:status |
  | active   |

# No authentication required
When (No Actor) health check, call table:
  | |

Even when there are no parameters, an empty DataTable is required to satisfy the syntax.

Time Format

Time values in ApiCall are automatically converted based on the time_format setting in isa.yml:

FormatOutput Example
ISO (default)"2025-12-25T14:30:00+08:00"
TIMESTAMP1766735400000
EPOCH1766735400
DATE_ONLY"2025-12-25"
TIME_ONLY"14:30:00"

Error Handling

Lint Phase

ErrorDescription
Field not foundField name in DataTable not found in API Spec
Missing required fieldRequired parameter marked in OpenAPI is not provided (can be relaxed in specific scenarios with @allow-missing-parameters)
Extract field not foundField specified with < not found in Response Schema

@allow-missing-parameters: Allow Parameter-Validation Preconditions

When your test goal is to verify that the API rejects missing/invalid required parameters, you can add @allow-missing-parameters on an Example (or Scenario). This skips the required-parameter check for that scenario, while keeping other contract checks enabled.

Feature: Query API parameter validation

  Rule: Required parameter validation rules

    Example: Should succeed with complete parameters
      When (UID="$Alice.id") query data, call table:
        | Q:startDate | Q:endDate  | Q:pageNo | Q:pageSize |
        | 2024-01-01  | 2024-01-31 | 1        | 10         |
      Then query data(200) response, with table:
        | success |
        | true    |

    @allow-missing-parameters
    Example: Should fail when required parameter startDate is missing
      When (UID="$Alice.id") query data, call table:
        | Q:endDate  | Q:pageNo | Q:pageSize |
        | 2024-01-31 | 1        | 10         |
      Then query data(400) response, with table:
        | errorCode                  |
        | MISSING_REQUIRED_PARAMETER |

When to Use

  • Missing an OpenAPI required parameter (for example, missing Q:startDate)
  • Required parameter value is invalid, and your backend returns a "missing/invalid parameter" type of error

When Not to Use

  • Business rule validation failures (for example, query range exceeds 6 months)
  • Authorization or authentication failures
  • Invalid data state (for example, duplicate request)
  • Parameters are complete and well-formed, but rejected by business logic

Runtime

ErrorDescription
API not foundSummary not found in API Spec
Duplicate summaryMultiple operations use the same summary, cannot uniquely identify

Complete Example

Feature: Todo Management

  Background:
    Given current time is "2026-01-27T10:00:00"

    # EntitySetup instruction — prepare test data (see EntitySetup docs)
    # >Alice.id / <userId: extract userId from response into Alice.id variable
    Given prepare a user, with table:
      | >Alice.id | name  | email             | status |
      | <userId   | Alice | alice@example.com | ACTIVE |

  Example: Create and update a todo

    # (No Actor): API call without authentication
    # >AliceToken / <token: extract token from response
    When (No Actor) user login, call table:
      | >AliceToken | email             | password    |
      | <token      | alice@example.com | password123 |

    # (UID="$Alice.id"): call as Alice, Bearer Token injected automatically
    When (UID="$Alice.id") create todo, call table:
      | >todo1.id | title    |
      | <todoId   | Buy milk |

    # P: prefix for Path parameter; $todo1.id references a previously extracted variable
    When (UID="$Alice.id") update todo, call table:
      | P:todoId  | title        |
      | $todo1.id | Buy soy milk |

    # ResponseValidate instruction — verify response (see ResponseValidate docs)
    Then update todo(200) response, with table:
      | todoId    | title        | updatedAt                        |
      | $todo1.id | Buy soy milk | &sameTime("2026-01-27T10:00:00") |

On this page