Arazzo 1.1:有什么新内容,今天能运行什么

分享

2026 年 5 月 19 日,OpenAPI Initiative 发布了 Arazzo 1.1.0。 Arazzo 是 OpenAPI Initiative 用来描述一连串 API 调用的格式:先登录,再创建订单,然后付款,每一步把值传给下一步。OpenAPI 描述一个端点接受什么、返回什么;Arazzo 描述要调用哪些端点、按什么顺序,以及如何判断每次调用是否成功。

1.1 是自 2024 年的 1.0 以来第一个带来新功能的版本。我们针对自己的 API 写了一个 1.1 的工作流,用 10 月 9 日发布的 Redocly CLI 2.62.1 运行。runner 接受 1.1 文档,一个简单的增删改查工作流也能通过。但有两项新功能虽然能通过 linter,在真正运行时却被忽略了。 这篇文章介绍有哪些变化,提供一个可以下载运行的工作流,并指出目前最好避开的两项功能。

Arazzo 文件做什么

Arazzo 文档指向一个或多个 API 描述(通常是 OpenAPI 文件),并列出工作流。每个工作流是一串步骤。每一步通过 operationId 指定一个操作,填好参数和请求体,说明怎样算成功,并从响应中保存值供后续步骤使用。

这一个文件在三个地方都有用:

  • 测试。runner 针对真实服务器执行这些步骤,任何成功条件不满足就失败。下文就是这么用的。
  • 文档。读者能看到完成一项任务所需的调用顺序,这是 OpenAPI 文件表达不了的。
  • AI 智能体。读过 OpenAPI 的智能体知道每个端点做什么,但得自己猜怎么把它们组合起来。Arazzo 工作流会告诉它。

Arazzo 1.1 有哪些变化

最大的亮点是支持 AsyncAPI,这样一个工作流可以把 HTTP 调用和消息通道混在一起。其他大多数变化都让 HTTP 工作流更容易编写。全部变化见发布公告:

变化作用
type: asyncapi 来源来源描述现在除了 openapi 和 arazzo,还可以是 AsyncAPI 文档。
channelPath、action、correlationId、timeout发送或接收消息的步骤。action 是 send 或 receive,correlationId 把回复和请求对应起来,timeout 是以毫秒为单位的上限。
dependsOn一个步骤可以列出必须在它开始前完成的其他步骤。列出某个步骤并不会运行它。
动作上的 parameters调用另一个工作流的成功或失败动作可以给它传入参数,比如先刷新令牌再重试。
Selector Object用 JSONPath、XPath 或 JSON Pointer 从响应或输入中取值。可用于 outputs、参数值和请求体。
Expression Type Object固定查询语言的版本,比如 RFC 9535 定义的 JSONPath 或 XPath 3.1。取代 1.0 的 Criterion Expression Type Object。
in: querystring把一个参数作为整个查询字符串发送,与 OpenAPI 3.2 的同一新增内容对应。
$self标识文档的 URI,这样对它的引用就不取决于文件恰好存放在哪里。

规范还为 $steps.login.outputs.token 这样的运行时表达式补上了完整的语法,规定了解析来源描述的顺序,以及条件何时算为真的确切规则。这些没有增加字段,但解决了两个 runner 可能得出不同结论的情况。对 gRPC、GraphQL、MCP 和循环的支持在后续版本的路线图中,1.1 里没有。

一个可以在我们的 API 上运行的工作流

这个工作流在 api.testingapis.com 上创建一个商品,读回来,修改价格,删除它,再确认它已经不在了。它只用免费端点,所以不需要账号或 API 密钥就能运行。

products.arazzo.yaml
arazzo: 1.1.0
$self: https://testingapis.com/downloads/arazzo/products.arazzo.yaml
info:
  title: Product lifecycle on testingapis.com
  summary: Create a product, read it back, change its price, delete it and check that it's gone.
  version: 1.0.0
sourceDescriptions:
  - name: testingapis
    url: ./testingapis-products.openapi.json
    type: openapi
workflows:
  - workflowId: product-lifecycle
    inputs:
      type: object
      properties:
        title: { type: string, default: Arazzo test product }
        price: { type: number, default: 12.5 }
        newPrice: { type: number, default: 9.99 }
    steps:
      - stepId: createProduct
        operationId: Products_Create
        requestBody:
          contentType: application/json
          payload:
            title: $inputs.title
            price: $inputs.price
            category: electronics
        successCriteria:
          - condition: $statusCode == 201
        outputs:
          productId: $response.body#/data/id

      - stepId: readProduct
        operationId: Products_GetById
        parameters:
          - name: id
            in: path
            value: $steps.createProduct.outputs.productId
        successCriteria:
          - condition: $statusCode == 200
          - context: $response.body
            condition: $[?@.category == 'electronics' && @.inStock == false]
            type:
              type: jsonpath
              version: rfc9535
        onFailure:
          - name: cleanUp
            type: goto
            stepId: deleteProduct

      - stepId: changePrice
        operationId: Products_Update
        parameters:
          - name: id
            in: path
            value: $steps.createProduct.outputs.productId
        requestBody:
          contentType: application/json
          payload:
            title: $inputs.title
            price: $inputs.newPrice
            category: electronics
        successCriteria:
          - condition: $statusCode == 200
          - condition: $response.body#/data/price == $inputs.newPrice
        onFailure:
          - name: cleanUp
            type: goto
            stepId: deleteProduct

      - stepId: deleteProduct
        operationId: Products_Delete
        parameters:
          - name: id
            in: path
            value: $steps.createProduct.outputs.productId
        successCriteria:
          - condition: $statusCode == 200

      - stepId: confirmGone
        operationId: Products_GetById
        parameters:
          - name: id
            in: path
            value: $steps.createProduct.outputs.productId
        successCriteria:
          - condition: $statusCode == 404
    outputs:
      productId: $steps.createProduct.outputs.productId

把它和它指向的 OpenAPI 文件一起下载,放在同一个文件夹里运行。npx 会自动获取 runner,所以有 Node 22.12 或更高版本就够了:

Terminal
$ npx @redocly/cli@2.62.1 respect products.arazzo.yaml
输出(2026 年 10 月 13 日)
Running workflow products.arazzo.yaml / product-lifecycle

✓ POST /api/Products - step createProduct
  ✓ success criteria check - $statusCode == 201
  ✓ status code check - $statusCode in [200, 201, 400, 500]

✓ GET /api/Products/{id} - step readProduct
  ✓ success criteria check - $statusCode == 200
  ✓ success criteria check - $[?@.category == 'electronics' && @.inStock == fal...
  ✓ status code check - $statusCode in [200, 404, 500]

✓ PUT /api/Products/{id} - step changePrice
  ✓ success criteria check - $statusCode == 200
  ✓ success criteria check - $response.body#/data/price == $inputs.newPrice
  ✓ status code check - $statusCode in [200, 404, 500]

✓ DELETE /api/Products/{id} - step deleteProduct
  ✓ success criteria check - $statusCode == 200
  ✓ status code check - $statusCode in [200, 404, 500]

✓ GET /api/Products/{id} - step confirmGone
  ✓ success criteria check - $statusCode == 404
  ✓ status code check - $statusCode in [200, 404, 500]

Workflows: 1 passed, 1 total
Steps: 5 passed, 5 total
Checks: 12 passed, 12 total
Time: 922ms

文件里有几处做的事比看上去多:

  • $response.body#/data/id 是一个运行时表达式,# 后面跟着 JSON Pointer。我们的创建端点把新商品包在 { message, data } 里,所以 ID 在 /data/id。后面的每一步都以 $steps.createProduct.outputs.productId 读取它。
  • readProduct 中的条件用新的 Expression Type Object 声明查询是 RFC 9535 的 JSONPath。只要过滤器匹配到至少一个节点, JSONPath 条件就通过,所以 $[?@.category == 'electronics' && @.inStock == false] 一次检查了两个字段。我们还把期望值反过来,确认检查在该失败时会失败,结果确实失败了。
  • 每一步的第三项检查 status code check 并不在 Arazzo 文件里。它是 Redocly 根据 OpenAPI 文件列出的响应加上的,所以 API 没有记录的状态码会让该步骤失败,即使你自己的条件都通过了。

步骤失败时,工作流会收尾

在我们 API 上创建的商品是共享的:在被删除之前,任何列出商品的人都能看到你的商品。中途停下的测试会留下垃圾,所以 readProduct 和 changePrice 都有一个跳到 deleteProduct 的 onFailure 动作。为了验证,我们把价格检查改成了不可能通过:

有步骤失败时的输出
✗ PUT /api/Products/{id} - step changePrice
  ✓ success criteria check - $statusCode == 200
  ✗ success criteria check - $response.body#/data/price == $inputs.price
  ✓ status code check - $statusCode in [200, 404, 500]

Running failure action cleanUp for the step changePrice

✓ DELETE /api/Products/{id} - step deleteProduct
  ✓ success criteria check - $statusCode == 200

✓ GET /api/Products/{id} - step confirmGone
  ✓ success criteria check - $statusCode == 404

工作流仍然报告失败,但商品会先被删除。任何在共享环境中创建数据的工作流都应该这样做。

Selector Object 能过 lint,但不会执行

Selector Object 是从响应中取值的新方式。我们用它通过 JSON Pointer 读取商品的评分,并发送到我们的 echo 端点,该端点会原样返回收到的内容:

selector.arazzo.yaml(步骤)
- stepId: getProduct
  operationId: Products_GetById
  parameters:
    - { name: id, in: path, value: 1 }
  outputs:
    rating:
      context: $response.body
      selector: /rating/rate
      type: jsonpointer
- stepId: echoIt
  operationId: Echo_Echo_POST
  requestBody:
    contentType: application/json
    payload:
      rating: $steps.getProduct.outputs.rating

redocly lint 认为这个文件没有问题。echo 端点本应收到 {"rating": 4.3}。它实际收到的是:

runner 发送的请求体
{
  "rating": {
    "context": {
      "id": 1,
      "title": "Wireless Noise-Cancelling Headphones",
      "price": 249.99,
      ...
      "rating": { "rate": 4.3, "count": 127 },
      "inStock": true,
      "createdAt": "2025-06-12T10:30:00Z"
    },
    "selector": "/rating/rate",
    "type": "jsonpointer"
  }
}

runner 把 context 解析成了整个响应体,然后没有应用选择器,而是把 Selector Object 本身发了出去。输出里没有任何警告。 这一步之所以失败,只是因为我们检查了服务器收到的内容;一个只检查 200 的工作流会在发送错误数据的同时照样通过。

在 runner 跟上之前,请使用 1.0 的写法。对 JSON Pointer 来说,它反而更短:

在 1.0 和 1.1 中都可用
outputs:
  rating: $response.body#/rating/rate

传给工作流的参数丢失了

公告里给 parameters 举的例子是:一个失败动作调用刷新令牌的工作流,然后重试该步骤。我们的免费端点不签发令牌,所以我们用一个不存在的商品测试了同样的机制: 失败动作调用一个 report 工作流,它把输入 reason 发送到 echo 端点。

params.arazzo.yaml(步骤)
- stepId: getMissing
  operationId: Products_GetById
  parameters:
    - { name: id, in: path, value: 99999 }
  successCriteria:
    - condition: $statusCode == 200
  onFailure:
    - name: reportAndRetry
      type: retry
      retryLimit: 1
      workflowId: report
      parameters:
        - name: reason
          value: not found

runner 确实按规范描述的顺序调用了 report 工作流,然后重试了该步骤。但 echo 端点收到的是一个空对象:

echo 端点收到的内容
"contentType": "application/json",
"contentLength": 2,
"body": "{}",
"json": {}

reason 始终没有到达。单独用 --input reason=... 运行 report 是正常的,所以问题出在从动作传递 parameters 这一环。放到刷新令牌的场景,就意味着刷新请求发出去时没带 refresh token。

Redocly CLI 2.62.1 中能运行的功能

Arazzo 1.1 功能Lint运行
arazzo: 1.1.0接受能运行
$self接受接受;运行时没有可观察的效果
Expression Type Object接受能运行,该失败时会失败
dependsOn接受接受;反正步骤本来就按顺序运行
Selector Object接受未生效:对象被当作数据发送
动作上的 parameters接受工作流会运行,但输入为空
AsyncAPI 步骤未测试未测试

我们没有在 Redocly 的 changelog 里找到声称支持 Arazzo 1.1 的条目,所以这些可能只是尚未实现的功能。Arazzo 仓库里列出的其他工具可能表现不同,我们只测试了 Redocly。

现在编写 1.1 工作流

  • 使用 arazzo: 1.1.0 和能运行的功能:Expression Type Object、$self 和 dependsOn。
  • 用 $response.body#/data/id 这样的运行时表达式读取响应中的值,而不是 Selector Object。
  • 通过在某个步骤中重新登录来处理令牌刷新,而不是靠失败动作上的 parameters。
  • 检查服务器收到了什么,而不只是状态码。通过的 lint 和 200 都没发现这篇文章里的问题,我们的 echo 端点发现了。
  • 像我们用 @redocly/cli@2.62.1 那样固定 runner 的版本,这样 runner 更新就不会在没有提交的情况下改变你的结果。

如果你的工作流测试的是增删改查的完整流程,我们的 CRUD 测试课程讲了每一步需要哪些检查。读懂 OpenAPI 规范(英文)解释了工作流引用的 operationId,商品 API 文档列出了上面工作流涉及的每个字段。