Skip to main content

HTTP Step Type: Call HTTP Endpoints from Workflows and Custom Commands

· 3 min read
Erik Osterman
Founder @ Cloud Posse

Workflows and custom commands now support a native http step type that performs an HTTP request — any verb, query-string parameters, headers, and a request body (raw or form/JSON) — with per-attempt timeouts and retries that compose with the existing retry: policy. No more shelling out to curl. (Prefer type: webhook? It's an accepted alias.)

The Problem

Calling an external endpoint from a workflow used to mean a shell step running curl:

steps:
- type: shell
command: |
curl -sf -X POST "https://ci.example.com/hook" \
-H "Authorization: Bearer $TOKEN" \
-d '{"status":"deployed"}'

That works until it doesn't: curl isn't guaranteed to be on the box (especially on Windows), quoting and templating the payload is fiddly, and you get no first-class handling of timeouts or transient failures. Retrying a flaky 5xx meant hand-rolling a bash loop.

The Solution

The http step makes HTTP a first-class citizen:

workflows:
notify:
steps:
- name: trigger
type: http
url: "https://ci.example.com/hook/{{ .env.JOB_ID }}"
method: POST
query:
ref: "{{ .env.GIT_SHA }}"
headers:
Authorization: "Bearer {{ .env.TOKEN }}"
Content-Type: application/json
body: '{"status":"deployed"}'
expect:
status: [200, 201, 202, 204]
response:
- /"status"\s*:\s*"deployed"/
timeout: 30s
retry:
max_attempts: 5
backoff_strategy: exponential
initial_delay: 1s
max_delay: 30s

- name: report
type: info
content: "Endpoint returned HTTP {{ .steps.trigger.metadata.status_code }}"

Everything templates, including the URL, headers, query params, and body — so you can thread values from earlier steps or the environment straight into the request.

Sending Parameters

You asked for full parameter support, and it's all here:

  • Verbmethod: accepts GET (default), POST, PUT, PATCH, DELETE, HEAD, and OPTIONS.
  • Query-string parametersquery: is a key-value map appended to the URL.
  • POST parameters — use body: for a raw payload, or form: for a key-value map. form is sent as application/x-www-form-urlencoded by default, or as a JSON object when you set a JSON Content-Type:
steps:
- name: notify-slack
type: http
url: "{{ .env.SLACK_WEBHOOK_URL }}"
method: POST
headers:
Content-Type: application/json
form:
text: "Deployment complete"

Timeouts and Retries That Compose

timeout is the per-attempt deadline. retry is the same retry block you already use on shell and atmos steps — and the http step is HTTP-aware about what's worth retrying:

  • Retried by default: transport/network errors, 5xx, and 429 Too Many Requests.
  • Fail fast: other 4xx responses (a 404 won't burn through five attempts).
  • Your call: retry.conditions regexes (matched against "<status> <body>") let you retry additional cases, and expect.status / expect.response define exactly what counts as success.

This makes the step a natural fit for polling, too — GET a health endpoint and retry until the body matches:

steps:
- name: health
type: http
url: "{{ .env.HEALTH_URL }}"
expect:
status: [200]
response:
- /"status"\s*:\s*"(ok|healthy)"/
retry:
max_attempts: 10
backoff_strategy: constant
initial_delay: 2s

Using the Response

The response body becomes the step's value, and useful details land in metadata:

  • {{ .steps.<name>.value }} — the response body
  • {{ .steps.<name>.metadata.status_code }} — the numeric status code
  • {{ .steps.<name>.metadata.status }} — the status text (e.g., 200 OK)
  • {{ .steps.<name>.metadata.response_headers }} — response headers

Because it's a regular step type, http works in custom commands exactly as it does in workflows.

Get Involved

See the workflow step types and custom commands docs for the full reference, and the examples/http-webhooks example to try it out. Questions or ideas? Join us in the Cloud Posse community Slack.