Skip to main content

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

· 4 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.

Here's the http step calling a local endpoint end to end:

atmos HTTP webhooks
 
00:00.0 / 00:00.0

View the full example

Sending Parameters​

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

  • Verb — method: accepts GET (default), POST, PUT, PATCH, DELETE, HEAD, and OPTIONS.
  • Query-string parameters — query: 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.

For usage and configuration, see http.

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.