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 |
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 |
|---|---|---|
| (no secret) | — |
| key, location (header | query), fieldName | key |
| token | token |
| 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 |
Per Item | N to N | one call per incoming item ( |
Reducer | N to 1 | one call with all 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"
]
}.webp)
HTTP Node can be used to call whitelisted domains, Eg: Here it calling Atlassian and Slack APIs
.webp)
Each HTTP node’s stream behavior can be selected, most use case are covered by per-item, expander and none.
.webp)
Each HTTP node’s stream behavior can be selected, most use case are covered by per-item, expander and none.(1).webp)
We can drag and drop previous nodes values to use it in the bodies or params or the request..webp)
.webp)
We can check the input output of the nodes in this plane.