Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Calling HTTP services

This guide shows how to call an HTTP endpoint from a workflow without writing a worker, using the built-in task orbit.http.request. You enable the adapter on the server, run the health-check example, post a JSON webhook with headers and retries, and learn how the adapter maps HTTP statuses to step results.

When to use the adapter

The HTTP adapter runs inside the runtime process. It suits calls that need no custom code around them: webhooks, health checks, and simple JSON APIs. Write a worker instead when the call needs request signing, pagination, response transformation, client certificates, secrets that should not pass through workflow input, or any business logic.

Enable the adapter

The adapter is off by default, because it lets workflow authors make outbound requests from the runtime host. Start the server with --enable-http-adapter:

orbit server --enable-http-adapter
INFO orbit: starting ORBIT runtime db=orbit.db backend="sqlite" http_adapter=true auth=false ui=true

If the adapter is disabled, a step that uses it fails immediately with the non-retryable code ADAPTER_DISABLED:

  ✗ probe                          failed the HTTP adapter is disabled; start the server with --enable-http-adapter

FAILED ping-off  HttpPing@1.0.0
  elapsed: 71ms
  compensation: none
  error: ADAPTER_DISABLED step `probe` failed: the HTTP adapter is disabled; start the server with --enable-http-adapter

With Docker, append the flag to the server command: docker run ... ghcr.io/ajmalx0/orbit:v0.1.0 server --enable-http-adapter.

The built-in contract

orbit.http.request is declared by the compiler’s prelude, so you use it without a task declaration:

task "orbit.http.request" {
    version "1.0.0"
    input {
        method: String = "GET"
        url: String
        headers: Map<String, String> = {}
        body: JSON = null
    }
    output {
        status: Int
        headers: Map<String, String>
        body: JSON
    }
}
Input fieldBehavior
methodGET (default), POST, PUT, PATCH, DELETE, HEAD or OPTIONS. Case does not matter. Any other method fails with a non-retryable HTTP_ERROR.
urlMust start with http:// or https://. Other schemes fail with a non-retryable HTTP_ERROR.
headersSent as request headers.
bodyFor POST, PUT and PATCH only. null sends an empty body, a string is sent as is, and any other value is sent as JSON.
Output fieldContents
statusThe HTTP status code.
headersResponse headers, with lowercase names.
bodyThe response body parsed as JSON, or the raw text as a string if it is not JSON. At most 8 MiB is read.

The step’s timeout applies to the whole request. Without one, a request may take up to 30 seconds. HTTPS certificates are verified against the public web PKI.

How statuses map to results

ResponseStep result
2xx, 3xx, 4xx other than 429The attempt succeeds. The workflow receives the status and decides what it means.
429 and 5xxThe attempt fails with a retryable HTTP_ERROR (server responded <status>), so the step’s retry policy applies. The status and body are kept in the error details.
Connection error or timeoutThe attempt fails with a retryable HTTP_ERROR (request failed: ...).

A 404 is therefore a successful step with status: 404. Test the status in return, in a when condition of a later step, or both.

Run the health-check example

examples/http_ping.orbit calls a URL and reports whether it answered 200:

// Calls an HTTP endpoint with the built-in adapter: no worker required.
// Start the runtime with `orbit server --enable-http-adapter`.

workflow HttpPing {
    version "1.0.0"
    input { url: String = "http://127.0.0.1:7233/v1/health" }

    step probe {
        worker "orbit.http.request"
        with { url: input.url, headers: { accept: "application/json" } }
        timeout 5s
        retry { attempts: 3, backoff: exponential, initial_delay: 200ms }
    }

    return {
        status: probe.status
        healthy: probe.status == 200
    }
}

The default URL is the runtime’s own health endpoint, so the example works with no other service. From a clone of the repository, with the server running:

orbit run examples/http_ping.orbit --id ping-1
deployed HttpPing@1.0.0 (sha256:0c0d025d4d99)
started execution ping-1
  ✓ probe                          succeeded 4ms

COMPLETED ping-1  HttpPing@1.0.0
  elapsed: 9ms
{
  "status": 200,
  "healthy": true
}

No worker was involved. orbit inspect ping-1 shows the request the runtime sent, with the input defaults applied, and the response it received (abridged):

"probe": {
  "status": "SUCCEEDED",
  "input": {
    "url": "http://127.0.0.1:7233/v1/health",
    "headers": { "accept": "application/json" },
    "method": "GET",
    "body": null
  },
  "output": {
    "status": 200,
    "headers": {
      "content-type": "application/json",
      "x-orbit-api-version": "orbit-api/1",
      "content-length": "140",
      "date": "Sun, 15 Nov 2026 09:37:40 GMT"
    },
    "body": {
      "status": "ok",
      "api_version": "orbit-api/1",
      "runtime_version": "0.1.0",
      "ir_versions": ["orbit-ir/1"],
      "active_executions": 1,
      "worker_sessions": 0
    }
  },
  "worker_id": "orbit-runtime"
}

A path that does not exist returns 404, which completes the workflow with "healthy": false:

orbit run examples/http_ping.orbit --id ping-404 --input '{"url": "http://127.0.0.1:7233/nope"}'
COMPLETED ping-404  HttpPing@1.0.0
  elapsed: 56ms
{
  "status": 404,
  "healthy": false
}

A URL where nothing listens fails each attempt with a connection error. The step retries twice, with 200 ms and 400 ms of backoff, then fails:

orbit run examples/http_ping.orbit --id ping-down --input '{"url": "http://127.0.0.1:7999/"}'
  ✗ probe                          failed request failed: io: Connection refused (os error 111) (gave up after 3 attempts)

FAILED ping-down  HttpPing@1.0.0
  elapsed: 888ms
  compensation: none
  error: HTTP_ERROR step `probe` failed: request failed: io: Connection refused (os error 111) (gave up after 3 attempts)

Post a webhook

The following workflow posts a JSON event with an authorization header and an idempotency header, retries server errors, and reports whether the receiver answered 201 Created:

workflow NotifyWebhook {
    version "1.1.0"
    input {
        order_id: String
        webhook_url: String
        api_key: String
    }
    output { accepted: Bool, status: Int }

    step post_event {
        worker "orbit.http.request"
        with {
            method: "POST"
            url: input.webhook_url
            headers: {
                authorization: "Bearer " + input.api_key
                "idempotency-key": "order-created-" + input.order_id
            }
            body: { event: "order.created", order_id: input.order_id }
        }
        timeout 10s
        retry { attempts: 3, backoff: exponential, initial_delay: 1s }
    }

    return {
        accepted: post_event.status == 201
        status: post_event.status
    }
}
  • Header names that are not identifiers, such as idempotency-key, are written as quoted keys.
  • The adapter does not add an idempotency header by itself. Retries and redeliveries can send the same request more than once, so pass a stable key that the receiver can deduplicate on. Build it from workflow data, as here, or from execution.id.
  • body is a record, so it is sent as JSON.

Against a receiver that answers 201:

orbit run notify.orbit --id wh-3 --input '{"order_id": "A-1", "webhook_url": "https://hooks.example.com/orders", "api_key": "k-123"}'
deployed NotifyWebhook@1.1.0 (sha256:961caf628d35)
started execution wh-3
  ✓ post_event                     succeeded 5ms

COMPLETED wh-3  NotifyWebhook@1.1.0
  elapsed: 51ms
{
  "accepted": true,
  "status": 201
}

Against a receiver that answers 503, each attempt fails with a retryable error, and the step gives up after the third:

  ↻ post_event                     retry_wait server responded 503
  ↻ post_event                     retry_wait server responded 503
  ✗ post_event                     failed server responded 503 (gave up after 3 attempts)

FAILED wh-4  NotifyWebhook@1.1.0
  elapsed: 3.3s
  compensation: none
  error: HTTP_ERROR step `post_event` failed: server responded 503 (gave up after 3 attempts)
+    70ms   #5  StepAttemptFailed         step=post_event attempt=1 error=HTTP_ERROR server responded 503
+    71ms   #6  RetryScheduled            step=post_event delay_ms=1000
+    1.2s   #9  StepAttemptFailed         step=post_event attempt=2 error=HTTP_ERROR server responded 503
+    1.2s  #10  RetryScheduled            step=post_event delay_ms=2000
+    3.3s  #13  StepAttemptFailed         step=post_event attempt=3 error=HTTP_ERROR server responded 503
+    3.3s  #14  StepFailed                step=post_event error=HTTP_ERROR server responded 503 (gave up after 3 attempts)

Secrets and the adapter

Everything a step sends is part of the execution’s history: the workflow input in WorkflowCreated, and the evaluated step input, including headers, in StepScheduled. orbit logs, orbit logs --json and orbit inspect hide values whose keys look sensitive, such as api_key and authorization:

"headers": { "authorization": "***", "idempotency-key": "order-created-A-1" }

That redaction applies to the CLI’s output only. The database and the HTTP API (GET /v1/executions/{id} and /history) hold the values in clear text, and anyone with the admin token can read them. For long-lived credentials, call the service from a worker that reads the secret from its own environment instead of passing it through workflow input.

Security checklist

  • Enable the adapter only on runtimes whose workflow authors you trust to make outbound requests from the runtime host.
  • The adapter accepts any http:// or https:// URL, including internal addresses reachable from the runtime host. Restrict the host’s egress with a firewall or proxy if workflows must not reach internal services.
  • Prefer https:// URLs. Plain http:// sends headers and bodies unencrypted.

See also