Skip to content

feat: added support for AsyncAPI v3#367

Open
nashjain wants to merge 5 commits intoOAI:v1.1-devfrom
specmatic:dev
Open

feat: added support for AsyncAPI v3#367
nashjain wants to merge 5 commits intoOAI:v1.1-devfrom
specmatic:dev

Conversation

@nashjain
Copy link

@nashjain nashjain commented Sep 9, 2025

  • Added support for AsyncAPI v3
  • Also cleaned up the spec to make it very clear that step-object can be oneOf openapi-step-object or asyncapi-step-object or workflow-step-object
  • For AsyncAPI we really need support for timeout, fork and join. However, these are also useful for OpenAPI so added it at the base step object.
  • For OpenAPI we need at least one successCriteria but for AsyncAPI it can be optional.

While we've tried to incorporate as much as possible from #270 not everything is covered.

@kevinduffey
Copy link
Collaborator

Let's have you join Nick, Mike and myself next week and the next few weeks for some follow up on how this works. We started meeting to discuss the best approach forward as well. As this would fundamentally alter the spec we'll definitely need a bit of time to go through what you have here and what we are looking in to.. see what might overlap, etc. Shoot me a msg on slack with your email so I can add you to our conversations there.

@nashjain
Copy link
Author

Here is a simple example just for your reference.

arazzo: "1.0.1"
info:
  title: "Workflow for placing an Order"
  version: "1.0.0"
sourceDescriptions:
- name: "OrderApi"
  url: "./openapi/order.yaml"
  type: "openapi"
- name: "AsyncOrderApi"
  url: "./asyncapi/order.yaml"
  type: "asyncapi"
workflows:
- workflowId: "PlaceOrder"
  inputs:
    required:
    - "CreateOrder"
    type: "object"
    properties:
      CreateOrder:
        required:
        - "orderRequestId"
        - "productId"
        - "quantity"
        type: "object"
        properties:
          orderRequestId:
            type: "string"
          productId:
            type: "integer"
          quantity:
            type: "integer"
  steps:
  - stepId: "CreateOrder"
    operationId: "$sourceDescriptions.AsyncOrderApi.PlaceOrder"
    stepType: "asyncapi"
    action: "send"
    parameters:
    - name: "orderRequestId"
      in: "header"
      value: "$inputs.CreateOrder.orderRequestId"
    requestBody:
      payload:
        productId: "$inputs.CreateOrder.productId"
        quantity: "$inputs.CreateOrder.quantity"
    outputs:
      orderRequestId: "$message.header.orderRequestId"
  - stepId: "ConfirmOrder"
    operationId: "$sourceDescriptions.AsyncOrderApi.PlaceOrder"
    stepType: "asyncapi"
    action: "receive"
    correlationId: "$steps.CreateOrder.outputs.orderRequestId"
    timeout: 6000
    outputs:
      orderId: "$message.body.orderId"
  - stepId: "GetOrderDetails"
    operationId: "$sourceDescriptions.OrderApi.getOrder"
    parameters:
    - name: "orderId"
      in: "path"
      value: "$steps.ConfirmOrder.outputs.orderId"
    successCriteria:
    - condition: "$statusCode == 200"
components: {}

@nashjain
Copy link
Author

As discussed, here is an example with fork and join

arazzo: "1.0.1"
info:
  title: "Workflow for placing an Order"
  version: "1.0.0"
sourceDescriptions:
- name: "OrderApi"
  url: "./openapi/order.yaml"
  type: "openapi"
- name: "AsyncOrderApi"
  url: "./asyncapi/order.yaml"
  type: "asyncapi"
workflows:
- workflowId: "PlaceOrder"
  inputs:
    required:
    - "CreateOrder"
    type: "object"
    properties:
      CreateOrder:
        required:
        - "orderRequestId"
        - "productId"
        - "quantity"
        type: "object"
        properties:
          orderRequestId:
            type: "string"
          productId:
            type: "integer"
          quantity:
            type: "integer"
  steps:
  - stepType: "asyncapi"
    stepId: "ConfirmOrder"
    operationId: "$sourceDescriptions.AsyncOrderApi.PlaceOrder"
    action: "receive"
    correlationId: "$inputs.CreateOrder.orderRequestId"
    fork: true # Converts ConfirmOrder to a Non Blocking Step
    timeout: 6000
    outputs:
      orderId: "$message.body.orderId"
  - stepType: "asyncapi"
    stepId: "CreateOrder"
    operationId: "$sourceDescriptions.AsyncOrderApi.PlaceOrder"
    action: "send"
    parameters:
    - name: "orderRequestId"
      in: "header"
      value: "$inputs.CreateOrder.orderRequestId"
    requestBody:
      payload:
        productId: "$inputs.CreateOrder.productId"
        quantity: "$inputs.CreateOrder.quantity"
  - stepId: "GetOrderDetails"
    operationId: "$sourceDescriptions.OrderApi.getOrder"
    join: # Waits for ConfirmOrder to complete or timeout
     - ConfirmOrder # Can also be 'true' to join/wait all
    parameters:
    - name: "orderId"
      in: "path"
      value: "$steps.ConfirmOrder.outputs.orderId"
    successCriteria:
    - condition: "$statusCode == 200"
components: {}

@frankkilcommins
Copy link
Collaborator

Thanks @nashjain - I will try to propose changes to the specification based on the examples later this week. There will no doubt be a few finer grained points that we'll need to discuss

@frankkilcommins
Copy link
Collaborator

frankkilcommins commented Oct 29, 2025

Taking the above examples into consideration here's a proposal outlining the specification changes to support AsyncAPI in v1.1.0. I've also provided an updated example which complies to the this proposal below. If we're in general agreement, I'll update this PR to reflect the proposal.

Summary of the specification changes to add AsyncAPI support (not exhaustive)

Source Description Object

type

Updated Allowed Values:

  • openapi
  • arazzo
  • asyncapi (new)

Description/Rationale:
The addition of asyncapi enables referencing AsyncAPI 3.0.0 definitions and allows workflows to model message-driven systems in addition to traditional request/response patterns.

Runtime Expressions

$message

Type: object
Available In: Steps with kind: asyncapi and action: receive
Description:
Provides access to the body and headers of a message received via an AsyncAPI-defined channel. This runtime expression enables event-driven workflows to assert success or extract outputs from message data.

Example:

successCriteria:
  - condition: $message.payload != null
  - condition: $message.header.correlationId == 'abc123'

$elapsedTime (optional - nice to have)

Type: integer
Available In: After a step completes
Description:
Represents the total time (in milliseconds) that a step took to execute. Can be used in successCriteria and onFailure.criteria to enforce nuanced timeout behaviour or performance related onFailure / onSuccess behaviour.

Example:

successCriteria:
  - condition: $elapsedTime < 5000

Step Object

kind

Type: string
Allowed Values: openapi, asyncapi, workflow
Description:
Indicates the type of step. Required when the document contains multiple sourceDescriptions of different types. Enables correct interpretation of fields like operationId, action, etc. We'll clearly explain to implementors how to infer this if omitted and what defaults should be. The generally thought process is that this is good moving forward but we can't make mandatory in minor release. It should be present for those looking to express async types of steps

action

Type: string
Allowed Values: send, receive
Required If: kind is asyncapi
Description: |
Indicates whether the step is sending (publishing) or receiving (subscribing to) a message on a channel described in an AsyncAPI document. Also enables explicit usage mode of webhooks and/or callbacks described in OpenAPI.

timeout

Type: integer
Format: milliseconds
** Description:** |
Defines the maximum allowed execution time for a step in milliseconds. If the step does not complete within the specified duration, it is considered a failure. The default behavior upon timeout is to terminate the workflow equivalent to using an end failure action.

Example:

timeout: 5000

Timeout behavior can be overridden by defining an onFailure block with criteria based on $elapsedTime. This would allow retry behaviour etc. if needed.

Example:

onFailure:
  - name: retryTimeout
    type: retry
    retryLimit: 3
    retryAfter: 1000
    criteria:
      - condition: $elapsedTime >= 5000

correlationId

Type: string (value runtime expression or literal)
Description:
Used in asyncapi steps to associate messages across send/receive operations. Typically references an ID passed in the message header or payload to correlate requests and responses.

Example:

correlationId: $inputs.CreateOrder.orderRequestId

dependsOn

Type: string array
Description:
Specifies a list of step identifiers that must complete (or be waited for) before the current step can begin execution. This enables modelling of explicit execution dependencies within a workflow. Note about forking: we leaning towards not having an explicit fork property in asyncapi kind steps. Instead we can assume that any type of such step with action: receive is by default non-blocking (or asynchronous) in nature. Other steps can leverage dependsOn to ensure the joining type of behaviour.

Example:

dependsOn:
  - $steps.ConfirmOrder

Step Execution Semantics

A step is considered successful only when all successCriteria are satisfied. If any condition fails, the step is deemed to have failed, and onFailure logic (if defined) is evaluated and executed.

There is no dedicated timeout field.
Instead, timeout behavior must be expressed using $elapsedTime within the successCriteria.

Example:

successCriteria:
  - condition: $statusCode == 200
  - condition: $elapsedTime < 5000

onFailure:
  - name: handleTimeout
    type: end
    criteria:
      - condition: $elapsedTime >= 5000

Updated Example:

arazzo: 1.1.0
info:
  title: Workflow for placing an Order
  version: 1.0.0
sourceDescriptions:
  - name: OrderApi
    url: ./openapi/order.yaml
    type: openapi
  - name: AsyncOrderApi
    url: ./asyncapi/order.yaml
    type: asyncapi
workflows:
  - workflowId: PlaceOrder
    inputs:
      required:
        - CreateOrder
      type: object
      properties:
        CreateOrder:
          required:
            - orderRequestId
            - productId
            - quantity
          type: object
          properties:
            orderRequestId:
              type: string
            productId:
              type: integer
            quantity:
              type: integer
    steps:
      - kind: asyncapi
        stepId: ConfirmOrder
        operationId: $sourceDescriptions.AsyncOrderApi.PlaceOrder
        action: receive # Non Blocking Step by default
        timeout: 6000
        correlationId: $inputs.CreateOrder.orderRequestId
        successCriteria:
          - condition: $message.payload != null
        outputs:
          orderId: $message.body.orderId

      - kind: asyncapi
        stepId: CreateOrder
        operationId: $sourceDescriptions.AsyncOrderApi.PlaceOrder
        action: send
        parameters:
          - name: orderRequestId
            in: header
            value: $inputs.CreateOrder.orderRequestId
        requestBody:
          payload:
            productId: $inputs.CreateOrder.productId
            quantity: $inputs.CreateOrder.quantity

      - stepId: GetOrderDetails
        operationId: $sourceDescriptions.OrderApi.getOrder
        dependsOn:
          - $steps.ConfirmOrder
        parameters:
          - name: orderId
            in: path
            value: $steps.ConfirmOrder.outputs.orderId
        successCriteria:
          - condition: $statusCode == 200
components: {}

@nashjain
Copy link
Author

Thanks @frankkilcommins The proposal mostly looks good to me. I just need sometime to think through a couple of items. I'm currently in Australia. Next week, once I'm back we could jump on a call to discuss and close it.

@kevinduffey
Copy link
Collaborator

@nashjain We discussed a bit about the removal of fork: true and kind: async implicitly indicates a fork, so removing that and then using dependsOn instead of join since we have dependsOn in the spec already elsewhere. Also the nature of a "fire and forget" (dont need response) vs "fire and wait for a response" which basically assumes if a dependsOn isn't indicating a given async kind that has a correlation id (or maybe thats not even needed) that it would indicate a fire/forget scenario. What do you think? If you can join the next meeting we can discuss on that call that would be great.

@RomanHotsiy
Copy link

Do we even need kind on the step level? We have the specific source description encoded in the operationId field, why can't we just look it up there?

operationId: "$sourceDescriptions.AsyncOrderApi.PlaceOrder"


But I also have a more general question/concern. I'm not clear about tooling support requirements.

Provided AsyncAPI spec supports so many different protocols (websockets, kafka, mqtt, grpc, sns, sqs, etc, etc) would tooling be expected to support all of those? Or some subset? Or what?

My concern is we may have almost no support from tooling as I don't even understand how "receive" can be implemented for some of those protocols.

@frankkilcommins
Copy link
Collaborator

Do we even need kind on the step level? We have the specific source description encoded in the operationId field, why can't we just look it up there?

The proposal for kind is indeed an optional affordance that may:

  • simplify/improve validation and schema constructs
  • improve readability of Arazzo documents (for those wanting to read the YAML/JSON)
  • cater for extensibility of other step types (tbd if applicable for 1.1.0):
    • human / agent (in-the-loop steps)
    • wait / delay (temporal control steps)

It's not set in stone at this point and as you mention the type of API document can be used via the sourceDescription referenced by the operationId or operationPath. The value of keeping it may come down to how tooling and authors prefer to work, we’re open to feedback here.

But I also have a more general question/concern. I'm not clear about tooling support requirements.

Provided AsyncAPI spec supports so many different protocols (websockets, kafka, mqtt, grpc, sns, sqs, etc, etc) would tooling be expected to support all of those? Or some subset? Or what?

Arazzo itself is agnostic to the specific protocols that can be modelled within AsyncAPI. Tooling implementations may choose to support a subset of protocols depending on use case or runtime environment. We can look to state a documented set of "Recommended" protocols for tooling advertising Arazzo AsyncAPI support.

Off the top of my head (not final), something like:

Protocol (or format) Fit for Arazzo Notes
Kafka, AMQP, MQTT, NATS, WebSockets Recommended High interoperability, strong JSON alignment, stable delivery semantics
SNS, SQS, SSE Partial Supported but may require additional setup (e.g. polling for SQS)
Others (e.g. STOMP, Pulsar, Mercure) Not currently in scope

To help perhaps tease out some of the further details, I've created a repo with some examples. Would be great for others to chime in and/or review/enrich the examples. See https://github.com/frankkilcommins/arazzo-examples for details.

@mikeschinkel
Copy link

@frankkilcommins — +1 on kind.

@RomanHotsiy
Copy link

The proposal for kind is indeed an optional affordance that may:

simplify/improve validation and schema constructs

I disagree. This requires more validation in fact and makes it worse.
Now every tool should add some checks to ensure kind matches type? and provide a human readable error, etc.

See example below.

arazzo: 1.1.0
info:
  title: Workflow for placing an Order
  version: 1.0.0
sourceDescriptions:
  - name: OrderApi
    url: ./openapi/order.yaml
    type: openapi
  - name: AsyncOrderApi
    url: ./asyncapi/order.yaml
    type: asyncapi
steps:
      - kind: openapi # ❌ ooops, mismatch here between the source description `type` and `kind`, what do we do here?
        stepId: ConfirmOrder
        operationId: $sourceDescriptions.AsyncOrderApi.PlaceOrder
        action: receive # Non Blocking Step by default
        timeout: 6000
        correlationId: $inputs.CreateOrder.orderRequestId
        successCriteria:
          - condition: $message.payload != null
        outputs:
          orderId: $message.body.orderId

It just adds unnecessary duplication and also inconsistency:

  • using type in source description
  • using kind in step

improve readability of Arazzo documents (for those wanting to read the YAML/JSON)

Docs generators can display it nicely.

cater for extensibility of other step types (tbd if applicable for 1.1.0):
human / agent (in-the-loop steps)
wait / delay (temporal control steps)

This makes sense in general but then those steps won't be linked to any source descriptions so let's consider it later when those extensibility is introduced

@frankkilcommins — +1 on kind.

@mikeschinkel, any pros/cons you have in mind?

@mikeschinkel
Copy link

@RomanHotsiy — My +1 came from being on the bi-weekly Arazzo call with @frankkilcommins and @kevinduffey where we discussed this and @frankkilcommins explained the rationale which I remember seemed logical and useful and where the three of us agreed, though I do not remember the specifics.

I will let @frankkilcommins elaborate again, and/or maybe @kevinduffey can chime in.

If you are available Wednesday Nov 26th 7pm EET you could join the next call if you like.

@nashjain
Copy link
Author

nashjain commented Nov 26, 2025

@frankkilcommins @kevinduffey @mikeschinkel I've gone through the updates suggested by @frankkilcommins and it all makes sense to me. Good to go from my point of view. Once we agree on this, I can update the PR and also show a working demo of this spec with Specmatic in a couple of days.

@frankkilcommins
Copy link
Collaborator

Thanks @nashjain

Regarding kind, there are pros and cons, and I think it would be better to leave the addition of kind to when we're adding support for human in the loop or other kinds of steps (e.g., some folks have been asking for mcp steps too). I also think that having kind with options like openapi or asyncapi seems to be at the wrong level of granularity. IMHO, it would be more valuable to indicate something like api, workflow, human-in-loop, agent-in-loop, mcp etc.

To help evaluation, i've created another branch of the examples, which does not have the kind step property.

https://github.com/frankkilcommins/arazzo-examples/tree/without-kind/v1.1.0-prep/async-api-examples

cc @RomanHotsiy

@nashjain
Copy link
Author

nashjain commented Nov 26, 2025

I'm OK to drop kind, however, just want to call out 2 main advantages from my point of view:

  • As a tool author, makes my implementation logic simpler (I'm not hypothetically saying this, I've built a parse, workflow testing and mocking tool for Arazzo. We started without kind and added it as part of refactoring to simplify our code)
    • Yes, I agree it adds one more thing to check, but when someone makes a mistake as highlighted above, we still need to deal with it. So just skipping kind does not remove the need for validation and human readable error messages.
  • As a human reader of the spec, I don't need to deduce the step type by looking at source or presence of action. Just lot more straight forward to grok what is going on. declarative OVER imperative?

(I prefer calling it stepType to be consistent with the type declaration in the sourceDescriptions)

@nashjain
Copy link
Author

nashjain commented Dec 1, 2025

@frankkilcommins @kevinduffey @ndenny - as per our meeting on Nov 26th, I've updated the schema. Main changes:

  • Removed stepType/kind and instead made action mandatory for asyncapi set. This is used to distinguish the asyncapi step.
  • Replaced fork/join with dependsOn for better workflow management

| <a name="stepRequestBody"></a>requestBody | [Request Body Object](#request-body-object) | The request body to pass to an operation as referenced by `operationId` or `operationPath`. The `requestBody` is fully supported in HTTP methods where the HTTP 1.1 specification [RFC9110](https://tools.ietf.org/html/rfc9110#section-9.3) explicitly defines semantics for "content" like request bodies, such as within POST, PUT, and PATCH methods. For methods where the HTTP specification provides less clarity—such as GET, HEAD, and DELETE—the use of `requestBody` is permitted but does not have well-defined semantics. In these cases, its use SHOULD be avoided if possible. |
| <a name="stepSuccessCriteria"></a>successCriteria | [[Criterion Object](#criterion-object)] | A list of assertions to determine the success of the step. Each assertion is described using a [Criterion Object](#criterion-object). All assertions `MUST` be satisfied for the step to be deemed successful. |
| <a name="stepOnSuccess"></a>onSuccess | [[Success Action Object](#success-action-object) \| [Reusable Object](#reusable-object)] | An array of success action objects that specify what to do upon step success. If omitted, the next sequential step shall be executed as the default behavior. If multiple success actions have similar `criteria`, the first sequential action matching the criteria SHALL be the action executed. If a success action is already defined at the [Workflow](#workflow-object), the new definition will override it but can never remove it. If a Reusable Object is provided, it MUST link to a success action defined in the [components](#components-object) of the current Arazzo document. The list MUST NOT include duplicate success actions. |
| <a name="stepOnFailure"></a>onFailure | [[Failure Action Object](#failure-action-object) \| [Reusable Object](#reusable-object)] | An array of failure action objects that specify what to do upon step failure. If omitted, the default behavior is to break and return. If multiple failure actions have similar `criteria`, the first sequential action matching the criteria SHALL be the action executed. If a failure action is already defined at the [Workflow](#workflow-object), the new definition will override it but can never remove it. If a Reusable Object is provided, it MUST link to a failure action defined in the [components](#components-object) of the current Arazzo document. The list MUST NOT include duplicate failure actions. |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am wondering if you already discussed how asyncApi steps should behave with retry on Failure actions.
Will it subscribe again, or do any other actions? How to handle potentially duplicated messages in queues?
Maybe I am missing smth.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a great question. I don't think we've have explicitly discussed this. Here is my thinking:

  1. If a receive async step fails before an ack, retry would just try to fetch the message again from the topic/queue again. It is safe to retry.
  2. If a send async step fails, it could mean
    1. the message was never sent (in which case it is safe to retry) OR
    2. message was sent, but we are not sure what happened. However since we recommend sending a correlationId, even if a duplicate message was sent, the consumer can ignore it

Most messaging systems focus on delivery and ordering guarantees, not uniqueness. It is expected that the consumer uses the correlationId to check for duplicates.

Do you think Arazzo should do something special in this case?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nashjain what you state in your thinking seems reasonable to me.

Copy link
Contributor

@DmitryAnansky DmitryAnansky Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@frankkilcommins / @nashjain
I am missing clear vision on how a mix of OpenAPI and AsyncAPI steps should work.
Specifically the time when outputs set even with the usage of dependsON.
Could you please help me understand future flow for tooling?

Lets imagine the structure

Workflow:
steps:
- step1 (Async)
- step2 (Sync) + dependsOn(step1) - to outputs to be set
- step3 (Async)
- step4 (Sync)

In current scenario:

  1. should we pause execution on step2, as it dependsOn step1?
  2. should step3 be executed after step2 or step1 ?
  3. what if step1 can receive not only one response ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DmitryAnansky here is a working example I demonstrated at the OAI Conference in Paris last week. Also attached it the sequence diagram for this flow. Please go through this example and let me know if this makes sense?

arazzo: "1.0.0"
info:
  title: "Arazzo Workflow"
  version: "1.0.0"
sourceDescriptions:
- name: "LocationApi"
  url: "./openapi/location.yaml"
  type: "openapi"
- name: "ProductApi"
  url: "./openapi/product.yaml"
  type: "openapi"
- name: "AsyncOrderApi"
  url: "./asyncapi/order.yaml"
  type: "asyncapi"
- name: "WarehouseApi"
  url: "./openapi/warehouse.yaml"
  type: "openapi"
- name: "OrderApi"
  url: "./openapi/order.yaml"
  type: "openapi"
workflows:
- workflowId: "PlaceOrder"
  inputs:
    required:
    - "createOrderSend"
    - "getUserLocation"
    type: "object"
    properties:
      createOrderSend:
        required:
        - "requestId"
        type: "object"
        properties:
          requestId:
            type: "string"
      getUserLocation:
        required:
        - "userEmail"
        type: "object"
        properties:
          userEmail:
            type: "string"
            format: "email"
  steps:
  - stepId: "getUserLocation"
    operationId: "$sourceDescriptions.LocationApi.getUserLocation"
    parameters:
    - name: "userEmail"
      in: "query"
      value: "$inputs.getUserLocation.userEmail"
    successCriteria:
    - condition: "$statusCode == 200"
    outputs:
      userId: "$response.body#/userId"
      locationCode: "$response.body#/locationCode"
  - stepId: "getProducts"
    operationId: "$sourceDescriptions.ProductApi.getProducts"
    parameters:
    - name: "locationCode"
      in: "query"
      value: "$steps.getUserLocation.outputs.locationCode"
    successCriteria:
    - condition: "$statusCode == 200"
    onSuccess:
    - name: "IsArrayEmpty"
      type: "end"
      criteria:
      - condition: "$response.body#/0 == null"
    outputs:
      productId: "$response.body#/0/productId"
      inventory: "$response.body#/0/inventory"
  - stepId: "createOrderSend"
    operationId: "$sourceDescriptions.AsyncOrderApi.createOrder"
    action: "send"
    parameters:
    - name: "requestId"
      in: "header"
      value: "$inputs.createOrderSend.requestId"
    requestBody:
      payload:
        userId: "$steps.getUserLocation.outputs.userId"
        productId: "$steps.getProducts.outputs.productId"
        inventory: "$steps.getProducts.outputs.inventory"
    outputs:
      requestId: "$message.header.requestId"
  - stepId: "createOrderReceive"
    operationId: "$sourceDescriptions.AsyncOrderApi.createOrder"
    action: "receive"
    correlationId: "$steps.createOrderSend.outputs.requestId"
    timeout: 6000
    outputs:
      orderId: "$message.payload.orderId"
  - stepId: "reserveInventory"
    operationId: "$sourceDescriptions.WarehouseApi.reserveInventory"
    dependsOn:
    - "$steps.createOrderReceive"
    parameters:
    - name: "orderId"
      in: "query"
      value: "$steps.createOrderReceive.outputs.orderId"
    successCriteria:
    - condition: "$statusCode == 200"
  - stepId: "orderAccepted"
    operationId: "$sourceDescriptions.AsyncOrderApi.orderAccepted"
    action: "receive"
    correlationId: "$steps.createOrderSend.outputs.requestId"
    timeout: 6000
  - stepId: "outForDelivery"
    operationId: "$sourceDescriptions.AsyncOrderApi.outForDelivery"
    action: "send"
    dependsOn:
    - "$steps.createOrderReceive"
    parameters:
    - name: "requestId"
      in: "header"
      value: "$steps.createOrderSend.outputs.requestId"
    requestBody:
      payload:
        orderId: "$steps.createOrderReceive.outputs.orderId"
  - stepId: "getOrderDetails"
    operationId: "$sourceDescriptions.OrderApi.getOrderDetails"
    dependsOn:
    - "$steps.createOrderReceive"
    parameters:
    - name: "orderId"
      in: "path"
      value: "$steps.createOrderReceive.outputs.orderId"
    successCriteria:
    - condition: "$statusCode == 200"
components: {}

flow

Copy link
Contributor

@DmitryAnansky DmitryAnansky Dec 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for sharing the example.
It’s a bit hard to see the whole picture without more descriptive details.
Could you please shed more light on the case related to the following part of the flow:

So, we have this synchronous step.

  - stepId: "reserveInventory"
    operationId: "$sourceDescriptions.WarehouseApi.reserveInventory"
    dependsOn:
    - "$steps.createOrderReceive"

I can see that it dependsOn the createOrderReceive async step:

    - stepId: "createOrderReceive"
    operationId: "$sourceDescriptions.AsyncOrderApi.createOrder"
....

However, it is not clear from the execution diagram when the orderAccepted step should be executed.
Maybe you have a diagram with stepIds? This would be very helpful.

  - stepId: "orderAccepted"
    operationId: "$sourceDescriptions.AsyncOrderApi.orderAccepted"
....

If I understand it correctly, the steps in the example execute one by one, even though some of them are async?

Copy link
Collaborator

@frankkilcommins frankkilcommins left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nashjain can you please update the PR to target v1.1-dev branch? Thanks

@nashjain nashjain changed the base branch from dev to v1.1-dev December 17, 2025 16:45
kevinduffey
kevinduffey previously approved these changes Dec 24, 2025
Copy link
Collaborator

@kevinduffey kevinduffey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall looks fantastic. We've discussed a bunch and my only request at this stage is that we consider adding some sort of test details for future tool providers to utilize in ensuring their tool supports the addition of async.

src/arazzo.md Outdated
| <a name="stepTimeout"></a>timeout | `integer` | The maximum number of milli-seconds to wait for the step to complete before aborting and failing the step. Consequently this will fail the workflow unless failureActions are defined. |
| <a name="stepCorrelationId"></a>correlationId | `string` | A correlationId in AsyncAPI links a request with its response (or more broadly, to trace a single logical transaction across multiple asynchronous messages). Only applicable to `asyncapi` steps with action `receive` and has to be in-sync with correlationId defined in the AsyncAPI document. |
| <a name="stepAction"></a>action | `send or receive` | Describes the intent of the message flow. Indicates whether the step will send (publish) or receive (subscribe) to a message on a channel described in an AsyncAPI document, Only applicable for `asyncapi` steps. |
| <a name="stepDependsOn"></a>dependsOn | List[`string`] | A list of steps that MUST be completed before this step can be executed. Each value provided MUST be a `stepdId`. If you depend on a step from another workflow, you MUST use the full reference format with the workflow id. The keys MUST follow the regular expression: `^\$(?:workflows\.[^.]+\.)?steps\.[^.]+$`. |
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does introducing the step-level dependsOn implies parallel execution of steps? I was under the impression that steps are intended to be executed sequentially regardless whether they are rest or async.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tatomyr - thanks for raising this. It's an important point that we could clarify within the spec.

IMO, Introducing step-level dependsOn does not require parallel execution, but it does enable it.

Steps without dependsOn MAY be considered independent and thus MAY begin execution immediately, including in parallel, if tooling supports it.

Tools that supports only sequential execution MUST fall back to topologically sorted execution of steps, respecting all declared dependsOn relationships are respected.

It would be beneficial to include additional authoring guidance, something along the lines of:

If any step defines a dependsOn, authors SHOULD declare dependencies for all steps where execution order matters. Steps without dependsOn or implicit output references MAY be interpreted as suitable for parallel execution. Do not rely on the order of the steps array when using dependsOn.

The alternative to this would be to constrain dependsOn usage to asynchronous operations (akin to an await semantics). While that simplifies the model, it limits the utility of dependsOn for broader use cases that would benefit from more general dependency-driven execution, including parallelism and fan-in/fan-out coordination.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@frankkilcommins wouldn't this be a breaking change?

dependsOn does not require parallel execution, but it does enable it.

If we allow that, it can break existing workflows, as it introduces ambiguity in step sequencing that don't have the dependsOn relation. E.g., a workflow describing some CRUD sequence can potentially be executed simultaneously (unless dependsOn is explicitly added for each step), which might not be what the user expects.

Also, this looks to me overly complicated and error-prone, because it becomes harder to write, easier to miss something, and way harder to read, as now you'll have to do mental gymnastics to re-sort the steps.
Even in the example provided, the steps createOrderReceive and createOrderSend could potentially be executed in parallel (because there's no dependsOn declared between them), but it doesn't make much sense since the first does depend on the latter (so I guess it was supposed to have the dependsOn relation, but that was simply missed).

Again, what is the need for steps parallelism for AsyncAPI operations in the first place? If we do want parallelism, it can be achieved via different workflows.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tatomyr thanks for the response.

My comment was mainly highlighting that if we introduce dependsOn at the step level, then it's reasonable to expect similar semantics to dependsOn at the workflow level, where parallel execution is permitted (though not required and it's tooling-dependent). This potential wasn't something explicitly discussed when adding AsyncAPI support, so it definitely warrants further review. We should also re-evaluate whether timeout (or another mechanism) might already cover the core needs for async steps, without introducing additional complexity.

To your points:

wouldn't this be a breaking change?

Not necessarily, it depends on how we define the behaviour in the spec. Existing workflows that do not use dependsOn at the step level would continue to execute sequentially, as they always have. So there’s no break in behaviour unless authors opt into the new capability.

Also, this looks to me overly complicated and error-prone, because it becomes harder to write, easier to miss something, and way harder to read, as now you'll have to do mental gymnastics to re-sort the steps.

Yes, dependency-driven execution adds some complexity, but it’s an opt-in feature. Authors would only use step-level dependsOn when they want more control, such as when mixing asynchronous operations or orchestrating parallel execution. For most workflows, the default sequential model remains simpler and unchanged.

Steps like createOrderReceive and createOrderSend could now be executed in parallel

Yes, that’s a valid observation. The current example and PR may not fully account for how dependsOn will be interpreted with respect to parallelism. If we go forward with step-level dependsOn, we need to tighten both examples and authoring guidance to reflect the intended semantics clearly.

What is the need for step parallelism for AsyncAPI operations

There's no explicit need per se, but the possibility of parallelism naturally arises from the semantics of dependsOn, given how it behaves at workflow level. We shouldn’t assume this feature is only for AsyncAPI, it opens up broader orchestration patterns, which some use cases may benefit from.

So in summary, we need to review whether dependsOn at the step level is the right mechanism to solve the “await” behaviour needed for AsyncAPI support. If we proceed with it, the spec should clearly define its semantics, especially around sequencing and parallelism.

There will likely be a desire to align its behaviour with workflow-level dependsOn, but we can define clear boundaries and expectations if needed.

That said, it’s also worth exploring whether existing mechanisms like timeout (or a more focused addition), might already address the async coordination need, without adding new complexity to the step model.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@frankkilcommins thanks for the clarification.

If we proceed with it, the spec should clearly define its semantics

Yes, this is what I'd expect, along with an example demonstrating that.

it’s also worth exploring whether existing mechanisms like timeout (or a more focused addition), might already address the async coordination need

From the same example I've surmised that the correlationId/timeout do the job of coordinating the async calls, unless I'm missing something.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nashjain one last push needed here to get this over the line, but the points above are valid. Do you want to determine if the combination of correlationId, timeout, and the context that the operation is within an asyncapi type sourceDescription are enough for us to coordinate the async behaviour?

Or if we want to define very explicit semantics about a step-level dependsOn?

Also cleaned up the spec to make it very clear that step-object can be oneOf openapi-step-object or asyncapi-step-object or workflow-step-object
For AsyncAPI we really need support for timeout and dependOn. However, these are also useful for OpenAPI/Workflow steps so added it at the base step object.
For OpenAPI we need at least one successCriteria but for AsyncAPI it can be optional.
…le AsyncAPI specification for Pet Purchase API
… add pattern for dependsOn in schema.yaml to clarify that it can ref to a step from another workflow
kevinduffey
kevinduffey previously approved these changes Feb 3, 2026
Copy link
Collaborator

@kevinduffey kevinduffey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Copy link

@spirefyio spirefyio left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Get it done!

Copy link
Collaborator

@kevinduffey kevinduffey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

type: integer
dependsOn:
description: Specifies a list of step identifiers that must complete (or be waited for) before the current step can begin execution. If you depend on a step from another workflow, you MUST use the full reference format with the workflow id.
description: Specifies a list of step identifiers that must complete (or be waited for) before the current step can begin execution. Steps referred by dependsOn SHOULD be non-blocking/async steps. If you depend on a step from another workflow, you MUST use the full reference format with the workflow id.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please clarify, why step mentioned in dependsOn can't be synchronous?
Steps referred by dependsOn SHOULD be non-blocking/async steps.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like can async Step depend on Sync step results?

Copy link
Author

@nashjain nashjain Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically the spec does not stop you. But the intent is for a step (sync or async) to use the output of an asynchronous step. Hence we've used the language around SHOULD and not MUST.

With an async step, once you invoke it, you would move forward, as it is a non-blocking call. However, with a sync step, since it is blocking, you would anyway need to wait for it to complete.

Is there a use case you've in mind for which you want to depend on a sync step?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m wondering in what case we would need to make the dependency explicit. It seems redundant to me, since each step has to be executed (including setting the outputs) before we proceed to another step. Otherwise it will create the possibility to execute steps in different order (e.g., set the dependency to a next step).

Also, what does '... step identifiers that must complete (or be waited for)'? actually mean? How do we determine that an async step has completed? What exactly are we waiting for?

operationId: "$sourceDescriptions.AsyncOrderApi.outForDelivery"
action: "send"
dependsOn:
- "$steps.createOrderReceive"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need to reference $steps.createOrderReceive?
Since stepId is already unique, maybe it would make sense to use just createOrderReceive.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is existing syntax. Nothing to do with this PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it is not existing syntax.
Previously dependsOn property was only on workflows level, and syntax is like $sourceDescriptions.<name>.<workflowId>

A list of workflows that MUST be completed before this workflow can be processed. Each value provided MUST be a workflowId. If the workflow depended on is defined within the current Workflow Document, then specify the workflowId of the relevant local workflow. If the workflow is defined in a separate Arazzo Document then the workflow MUST be defined in the sourceDescriptions and the workflowId MUST be specified using a [Runtime Expression](https://spec.openapis.org/arazzo/latest.html#runtime-expressions) (e.g., $sourceDescriptions.<name>.<workflowId>) to avoid ambiguity or potential clashes.

Copy link
Author

@nashjain nashjain Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you see, $steps.<stepId>.outputs.<outputName> is widely used. So I simply picked $steps.<stepId> from there. That's what I meant by existing syntax.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nashjain
In your example "$steps.createOrderReceive" will evaluate to an object according to the existing specification of Runtime Expressions.
e.g. =>

$steps:
   stepId:
       response:
           .....
       outputs:
           someKey1: someValue1
           ......

Could you please clarify what does it even mean to dependsOn an object?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is likely due to how we describe what to do for workflow dependsOn:

A list of workflows that MUST be completed before this workflow can be processed. Each value provided MUST be a workflowId. If the workflow depended on is defined within the current Workflow Document, then specify the workflowId of the relevant local workflow. If the workflow is defined in a separate Arazzo Document then the workflow MUST be defined in the sourceDescriptions and the workflowId MUST be specified using a Runtime Expression (e.g., $sourceDescriptions.<name>.<workflowId>) to avoid ambiguity or potential clashes.

I think the intent is fairly clear but in general we should improve the ABNF grammar. Time permitting, I'll create a PR for #426 as part of the 1.1 efforts

- stepId: "reserveInventory"
operationId: "$sourceDescriptions.WarehouseApi.reserveInventory"
dependsOn:
- "$steps.createOrderReceive"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can it be a step from another workflow?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please also add an example with the usage?
Will it be like $workflows.{workflowId}.steps.{stepId} or from external file,
$sourceDescriptions.{name}.workflows.{workflowId}.steps.{stepId}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These examples are unit tests purely for testing purpose, not user documentation. We will be doing that separately.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to have this case covered in unit tests as well.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to this: As part of the 1.1 release, I am hoping that we can add an Arazzo section to the OAI learn site and have lots of good rich examples there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants