Documentation Index

Fetch the complete documentation index at: https://docs.safe.security/llms.txt

Use this file to discover all available pages before exploring further.

Using HTTP Request Node in Workflows

Prev Next

Overview

SAFE Workflows now supports calling external HTTP(S) APIs (Jira, Slack, and customer endpoints) directly from a workflow via a new HTTP Request node (ACTION_HTTP_REQUEST). Authentication is handled through a centrally managed, encrypted Credentials Library in admin-service; secrets are never exposed to the workflow builder and never leave storage in plaintext. A synchronous Test Request lets builders validate a call from the canvas before saving, running the identical hardened path as a real execution.

Key Capabilities

  • HTTP Request node: method, URL, query params, headers, and body (none / JSON / form-urlencoded / raw), referencing a credential.

  • Credentials Library: create / list / edit / delete credentials of type None, API Key (header or query), Bearer Token, Basic Auth, each bound to a mandatory allowed-domains allowlist.

  • Test Request: run an unsaved node config synchronously and see the (redacted) response, through the same security path as a real run.

  • Hardened egress: SSRF protection (DNS-pinned dialer + IP block-list + per-redirect re-check), per-credential exact-host allowlist, TLS validation, 30s timeout, 5-redirect and 1 MB caps.

  • Structured response + branching: every call returns a consistent envelope (ok, statusCode, headers, body, json, …); downstream nodes branch on {{ ok }}.

  • Stream behaviors: None / Expander / Per Item / Reducer for array-shaped responses.

HTTP Request Node

The ACTION_HTTP_REQUEST node is configured with a schema-driven form:

Field

Notes

Method

GET / POST / PUT / PATCH / DELETE / HEAD / OPTIONS

URL

Variable-picker enabled; validated at save (scheme, host, no userinfo, no control bytes)

Query Params / Headers

List-builder rows (key/value)

Body Type

none / json / form-urlencoded / raw (auto Content-Type)

Body

Supports {{ }} templates (masked at save, resolved at run)

Credential

Required; picked from the Credentials Library

Stream Behavior

None (default) / Expander / Per Item / Reducer, with an Items Path

Credentials Library

Credentials live in admin-service (admin_credentials, per-tenant DB) and are surfaced through a management UI. Sensitive values are encrypted by the Auth service (KMS-backed) and stored as ciphertext; reads return the secret masked.

Type

Config

Sensitive

none

(no secret)

api_key

key, location (header | query), fieldName

key

bearer_token

token

token

basic_auth

username, password

password

  • Allowed domains are mandatory (exact host, no wildcards, no empty): the credential can only be sent to those hosts.

Test Request

From the node config panel, Test Request runs the current (unsaved) config synchronously and returns the redacted response envelope. It resolves the referenced credential over a System-to-System call (the builder never sees the secret) and runs the identical core/httpnode egress path as a real run, closing the preview-endpoint bypass class. Security blocks (for example a host outside the allowlist) are shown to the tester; an unresolvable credential returns a clear error.

Response Handling & Branching

Every call returns a structured envelope:

ok (bool), statusCode, headers (sensitive headers redacted), body, json, durationMs, parseError, hostPath, errorCategory, errorMessage.

  • An HTTP response (any status, including 4xx/5xx/429/401) is a successful step; downstream nodes branch on {{ ok }}.

  • A security / config / infrastructure failure (SSRF block, credential-domain-not-allowed, TLS invalid, credential missing, decrypt failure, …) halts the run (the step fails, non-retryable): you cannot branch around a security block.

Stream Behaviors

Behavior

Cardinality

HTTP node does

None (default)

1 to 1

one call, returns the envelope

Expander

1 to N

fans a response array at itemsPath into a downstream stream

Per Item

N to N

one call per incoming item ({{ $item.* }})

Reducer

N to 1

one call with all items ({{ items }})

Security & Guardrails

  • SSRF: DNS-pinned net.Dialer.Control checks the actual connecting IP against a hardcoded block-list (loopback, RFC1918, link-local, CGNAT, IPv6 ULA/link-local, IPv4-mapped) at dial time (defeats DNS-rebinding TOCTOU); redirects re-check per hop; https to http downgrade blocked.

  • Credential exfiltration: per-credential exact-host allowlist, enforced before the secret is applied and on every redirect; Test Request inherits it.

  • Whitelisted Domains: Only a list of certain whitelisted domains are allowed to be called by this node, to get certain domain whitelisted contact customer success.

  • TLS validation on by default; 30s timeout; 5-redirect and 1 MB response caps.

  • No plaintext across boundaries: admin returns ciphertext, flow decrypts via Auth at point-of-use; secrets are never persisted, logged, or placed in Temporal payloads / step output.

  • RBAC: credential create/manage restricted to admin + TPRM Admin; node edit gated to workflow builders.

  • Redaction: Authorization, X-API-Key, API-Key, Cookie, Set-Cookie, WWW-Authenticate redacted in responses and logs; only credentialId is carried in workflow export.

Example

An Example JSON for creating JIRA task and post message on slack on Third Party status update

{
  "formatVersion": "1.0",
  "exportedAt": "2026-07-23T10:16:06Z",
  "source": {
    "workflowId": "19afa363-8879-4c5f-a71d-a38803eefa57",
    "versionId": "b6cc738c-eecd-4efd-839d-6c1964f71b49",
    "version": 29,
    "checksum": "65a62e6489347f5fa7b9f938af5c485930647fbdc0d01cb45a30118d613c6988"
  },
  "definition": {
    "id": "workflow-1784801739218",
    "name": "Third party Updated Jira/Slack",
    "isStarred": false,
    "tags": [],
    "tasks": [
      {
        "id": 2,
        "name": "Create Jira Task",
        "type": "ACTION_HTTP_REQUEST",
        "version": "1.0.0",
        "config": {
          "id": "action_http_request_2",
          "url": "https://safe-security.atlassian.net/rest/api/3/issue",
          "name": "Create Jira Task",
          "type": "ACTION_HTTP_REQUEST",
          "method": "POST",
          "behavior": "",
          "bodyType": "none",
          "itemsPath": "json",
          "credentialId": "22",
          "configureRequest": {
            "body": {
              "bodyJson": {
                "fields": {
                  "project": {
                    "key": "AMT"
                  },
                  "summary": "Task created to check vendor status",
                  "issuetype": {
                    "name": "Task"
                  }
                }
              },
              "bodyType": "json"
            },
            "authentication": {
              "credentialId": "22"
            }
          },
          "_httpSampleOutput": {
            "ok": true,
            "body": "",
            "json": {
              "id": "",
              "key": "",
              "self": ""
            },
            "headers": {
              "Nel": "",
              "Via": "",
              "Date": "",
              "Vary": "",
              "Server": "",
              "X-Cache": "",
              "Report-To": "",
              "Connection": "",
              "Set-Cookie": "",
              "Atl-Traceid": "",
              "X-Amz-Cf-Id": "",
              "Content-Type": "",
              "X-Aaccountid": "",
              "X-Amz-Cf-Pop": "",
              "X-Arequestid": "",
              "Cache-Control": "",
              "Server-Timing": "",
              "Atl-Request-Id": "",
              "Beta-Ratelimit": "",
              "X-Xss-Protection": "",
              "Timing-Allow-Origin": "",
              "Beta-Ratelimit-Policy": "",
              "X-Beta-Ratelimit-Limit": "",
              "X-Content-Type-Options": "",
              "Strict-Transport-Security": "",
              "X-Beta-Ratelimit-Remaining": ""
            },
            "hostPath": "safe-security.atlassian.net/rest/api/3/issue",
            "durationMs": 1468,
            "statusCode": 201
          }
        },
        "targets": [
          {
            "id": 7
          }
        ],
        "position": {
          "x": 450,
          "y": 150
        },
        "executionType": "DETERMINISTIC"
      },
      {
        "id": 6,
        "name": "Third Party Updated",
        "type": "TRIGGER_THIRD_PARTY_UPDATED",
        "version": "1.0.0",
        "config": {
          "id": "trigger_third_party_updated_6",
          "name": "Third Party Updated",
          "type": "TRIGGER_THIRD_PARTY_UPDATED",
          "eventType": "TRIGGER_THIRD_PARTY_UPDATED",
          "triggerFields": {
            "status": true
          }
        },
        "targets": [
          {
            "id": 2
          }
        ],
        "position": {
          "x": 250,
          "y": 150
        },
        "executionType": "DETERMINISTIC"
      },
      {
        "id": 7,
        "name": "Post to Slack",
        "type": "ACTION_HTTP_REQUEST",
        "version": "1.0.0",
        "config": {
          "id": "action_http_request_7",
          "url": "https://slack.com/api/chat.postMessage",
          "name": "Post to Slack",
          "type": "ACTION_HTTP_REQUEST",
          "method": "POST",
          "behavior": "",
          "itemsPath": "json",
          "configureRequest": {
            "body": {
              "bodyJson": "{\n  \"channel\": \"C0BGGSSQ24T\",\n  \"text\": \"Task created for checking vendor status - {{ $\"2\".json.self }}\"\n}",
              "bodyType": "json"
            },
            "authentication": {
              "credentialId": "33"
            }
          }
        },
        "targets": [],
        "position": {
          "x": 650,
          "y": 150
        },
        "executionType": "DETERMINISTIC"
      }
    ],
    "description": "test API node",
    "metadata": {},
    "createdAt": "2026-07-23T10:15:39.218Z",
    "updatedAt": "2026-07-23T10:15:39.218Z",
    "createdBy": {
      "userId": 0,
      "userEmail": ""
    },
    "updatedBy": {
      "userId": 0,
      "userEmail": ""
    }
  },
  "tags": [],
  "workflowMode": "AI_MANAGED",
  "usecases": [
    "TPRM"
  ]
}

HTTP Node can be used to call whitelisted domains, Eg: Here it calling Atlassian and Slack APIs

Each HTTP node’s stream behavior can be selected, most use case are covered by per-item, expander and none.

Each HTTP node’s stream behavior can be selected, most use case are covered by per-item, expander and none.

We can drag and drop previous nodes values to use it in the bodies or params or the request.



We can check the input output of the nodes in this plane.