Skip to content

Terraform

Async resource provisioning with polling

Many infrastructure APIs accept a create or delete request immediately and finish the work in the background. The API returns a 202 (or 204) with an identifier, and the resource is not actually usable, or fully removed, until a subsequent GET confirms the new state.

Speakeasy-generated Terraform providers can wait for those transitions automatically with the x-speakeasy-polling extension wired into the create and delete entity operations. This guide shows how to apply polling without touching the base OpenAPI document, by layering an overlay.

Scenario: a database that provisions asynchronously

Section titled “Scenario: a database that provisions asynchronously”

The base API exposes a Database resource with an asynchronous create and delete:

  • POST /databases returns 202 Accepted once the request is queued.
  • GET /databases/{id} returns the current status: pending, ready, or failed.
  • DELETE /databases/{id} returns 204 No Content while tear-down continues asynchronously.
paths:
/databases:
post:
operationId: createDatabase
x-speakeasy-entity-operation: Database#create
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/DatabaseRequest"
responses:
"202":
description: Database creation accepted; provisioning asynchronously.
content:
application/json:
schema:
$ref: "#/components/schemas/Database"
/databases/{id}:
parameters:
- name: id
in: path
required: true
schema:
type: string
get:
operationId: getDatabase
x-speakeasy-entity-operation: Database#read
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/Database"
"404":
description: Not found
delete:
operationId: deleteDatabase
x-speakeasy-entity-operation: Database#delete
responses:
"204":
description: Deletion accepted; tear-down is asynchronous.
components:
schemas:
Database:
type: object
required: [id, status]
properties:
id:
type: string
name:
type: string
status:
type: string
enum: [pending, ready, failed]

Apply polling as an overlay so the base spec stays untouched. The overlay does two things:

  1. Declares named polling strategies on the GET operation.
  2. Wires those strategies into the create and delete entity operations so Terraform waits after each lifecycle step.
overlay: 1.0.0
info:
title: Add async polling to Database
version: 0.0.0
actions:
# 1. Define polling strategies on the read operation.
- target: $.paths['/databases/{id}'].get
update:
x-speakeasy-polling:
- name: WaitForReady
successCriteria:
- condition: $statusCode == 200
- condition: $response.body#/status == "ready"
failureCriteria:
- condition: $statusCode == 200
- condition: $response.body#/status == "failed"
- name: WaitForDeleted
successCriteria:
- condition: $statusCode == 404
# 2. Attach the read operation as a follow-up to create and delete,
# using each polling strategy.
- target: $.paths['/databases/{id}'].get
update:
x-speakeasy-entity-operation:
- Database#read
- entityOperation: Database#create#2
options:
polling:
name: WaitForReady
delaySeconds: 2
intervalSeconds: 5
limitCount: 120
- entityOperation: Database#delete#2
options:
polling:
name: WaitForDeleted
intervalSeconds: 5
limitCount: 60

Apply the overlay alongside the base spec with speakeasy overlay apply -s openapi.yaml -o overlay.yaml, or attach it to a Terraform source with speakeasy configure sources.

  • After terraform apply creates a database, the provider calls GET /databases/{id} on a 5-second interval, after a 2-second initial delay, until status == "ready". Polling fails fast if status == "failed" or after 120 attempts.
  • After terraform destroy, the provider polls until the GET returns 404, confirming tear-down completed.
  • The #2 suffix on Database#create#2 and Database#delete#2 tells the generator to run the GET as a second step of the create or delete lifecycle. That is how the read operation is reused as a wait step without colliding with the normal Database#read.

The behavior of each strategy is controlled by a small set of fields on x-speakeasy-polling. Per-resource values under options.polling (shown in the overlay above) override the defaults declared on the operation.

  • delaySeconds: Initial wait before the first poll.
  • intervalSeconds: Time between polls.
  • limitCount: Maximum poll attempts before giving up.
  • successCriteria: All conditions must be true within a single poll response to stop polling successfully.
  • failureCriteria: All conditions must be true within a single poll response to stop polling and surface an error. Use multiple criteria to assert against the same response (e.g. $statusCode == 200 and $response.body#/status == "failed"); failure does not compose across separate polls.

Conditions use OpenAPI runtime expressions. The two most common contexts are $statusCode and $response.body#/<json-pointer>. See the polling reference for the full criterion grammar, including regex matching.