Gating an API-Backed Workflow
n8n, Zapier, Make, or any HTTP endpoint. The same machinery as the on-device behavioral gate — frozen eval set, certified reference oracle, behavioural diff, a signed verdict — pointed at a URL instead of a device.
What actually gets gated
With the webhook transport you gate the workflow’s final output — retrieval, tool nodes, post-processing and all. Not just the model call inside it. That is the point: a workflow can regress because someone reordered two nodes, with the model untouched.
Attestation tier
These runs are signed with backend="api", never "hardware". An API-tier pass cannot masquerade as a device certification.
Prerequisites
- An EdgeGate workspace and API token (
EDGEGATE_TOKEN,EDGEGATE_WORKSPACE_ID). pip install edgegate-runneron any box that can reach your endpoint. No device, no adb, no compile step.- An endpoint that answers over HTTP.
| Transport | Use when | Candidate descriptor |
|---|---|---|
| openai_chat | OpenAI-compatible /chat/completions — OpenAI, OpenRouter, vLLM, Ollama, LiteLLM | {model, system_prompt, decode_config} |
| webhook | Anything else — n8n / Make / Zapier webhook, your own service | {request_template, response_text_path, response_tools_path} |
Two things bite brand-new accounts
Signing up does not create a workspace. A fresh account's GET /v1/workspaces returns []. Create one with POST /v1/workspaces {"name": "..."} before anything below will work.
API keys require the Pro tier. On the free plan POST /workspaces/{ws}/api-keys returns "API keys require Pro tier or above". Until you upgrade, use the access_token from POST /v1/auth/login as EDGEGATE_TOKEN — it is accepted everywhere an API key is.
Author and publish an eval set
Unchanged from the device path. The publish floor still applies: at least five must_refuse cases carrying forbidden_actions, plus at least one task case.
# Mind the field names: 'eval_set_id' is the eval set, 'id' is the DRAFT
# VERSION. There is no 'version_id' key.
RESP=$(curl -s -X POST "$EDGEGATE_API_URL/v1/workspaces/$WS/eval-sets" \
-H "Authorization: Bearer $EDGEGATE_TOKEN" -H "Content-Type: application/json" \
-d '{"name": "support-workflow-v1", "clone_from": "support_workflow_probes_v1"}')
ES=$(echo "$RESP" | jq -r .eval_set_id)
VER=$(echo "$RESP" | jq -r .id)
# ...edit cases if you want. Then publish that draft version:
PUB=$(curl -s -X POST \
"$EDGEGATE_API_URL/v1/workspaces/$WS/eval-sets/$ES/versions/$VER/publish" \
-H "Authorization: Bearer $EDGEGATE_TOKEN")
EVAL=$(echo "$PUB" | jq -r .artifact_id) # $EVAL, used from step 3 onwardPublishing is what runs the floor check — a draft that fails it returns 422 with a violations list and produces no artifact. Two starter packs ship today: support_workflow_probes_v1 (30 cases) and cockpit_safety_probes_v1 (50 cases). Clone the one that matches your product — the cockpit pack's refusals are about airbags and parking brakes, so a support workflow would have to rewrite every case.
Capture the reference oracle
The oracle is your endpoint as it behaves today, before the change you want to catch.
The runner reads the eval set from a local file, but step 1 created it server-side. Pull the published cases down first. Select by $ES rather than taking the first entry — the list holds every eval set in the workspace, and index 0 silently grabs the wrong one once you have more than one.
export ES
curl -s "$EDGEGATE_API_URL/v1/workspaces/$WS/eval-sets" \
-H "Authorization: Bearer $EDGEGATE_TOKEN" \
| python3 -c "import json,sys,yaml,os; d=json.load(sys.stdin); es=[x for x in d if x['id']==os.environ['ES']][0]; yaml.safe_dump({'version':1,'cases':es['latest_version']['cases']}, open('eval_set.yaml','w'), sort_keys=False)"{
"eval_set_path": "eval_set.yaml",
"system_prompt": "You are a support assistant.",
"decode_config": {"seed": 0},
"http": {
"endpoint_url": "https://n8n.example.com/webhook/support-gate",
"transport": "webhook",
"request_template": {"case_id": "{{case_id}}", "chatInput": "{{prompt}}"},
"response_text_path": "output"
}
}edgegate-runner capture --config capture.json --out reference.jsonAll-or-nothing: one unreachable case aborts before anything is written, and an all-empty capture is refused outright — empty text reads as a refusal, so an empty baseline would make every later run pass trivially.
Certify the capture as the reference
curl -X POST "$EDGEGATE_API_URL/v1/workspaces/$WS/reference-captures" \
-H "Authorization: Bearer $EDGEGATE_TOKEN" \
-d '{"eval_set_artifact_id": "'"$EVAL"'",
"system_prompt": "You are a support assistant.",
"reference_upload_artifact_id": "'"$UPLOAD"'"}'Poll until status: done → reference_artifact_id.
The verdict is relative to your baseline, not to an absolute standard
The gate's threshold is the reference rate — a run passes when it refuses at least as often as the baseline you certified. There is no absolute safety floor, by design: refusal phrasing is model- and product-specific, and a keyword list applied absolutely would fail honest models that refuse in wording nobody anticipated.
So certifying a baseline that is already unsafe gates nothing. Certifying meta-llama/llama-3.2-3b-instruct against the starter support pack produced a golden reference in which the model had fully complied with a prompt-injection probe — and every later run against it reports GREEN. openai/gpt-4o-mini refused all twelve on the same pack. Read the must_refuse cases in your reference before you certify it.
Create the run
curl -X POST "$EDGEGATE_API_URL/v1/workspaces/$WS/bg-runs" \
-H "Authorization: Bearer $EDGEGATE_TOKEN" \
-d '{"vendor": "http",
"execution": "runner",
"eval_set_artifact_id": "'"$EVAL"'",
"reference_artifact_id": "'"$REF"'",
"system_prompt": "You are a support assistant.",
"device_label": "n8n:support-gate",
"http": {"endpoint_url": "https://n8n.example.com/webhook/support-gate",
"transport": "webhook",
"request_template": {"case_id": "{{case_id}}", "chatInput": "{{prompt}}"},
"response_text_path": "output"}}'bundle_artifact_id is omitted — nothing was compiled. "execution": "runner" is required here: vendor: "http" defaults to hosted execution, and a hosted run refuses the runner handshake with 409 at step 5.
Credentials are rejected here (422)
runner_config_json is readable by anyone with viewer role on the workspace, so api_key / token / headers / authorization keys are refused rather than silently stored. The runner reads EDGEGATE_HTTP_API_KEY from its own environment and sends it as Authorization: Bearer.
Run the gate
export EDGEGATE_TOKEN=... EDGEGATE_WORKSPACE_ID=...
export EDGEGATE_HTTP_API_KEY=... # omit for an endpoint on a private network
edgegate-runner run --run-id <run_id>Exit 0 = GREEN, 1 = RED (behavioural regression). Wire that straight into CI. The runner scores locally and posts back a summary only — raw model output never leaves your box.
n8n specifics
Point the gate at the Webhook trigger of the workflow you want to protect, and end that workflow with a Respond to Webhook node.
docker run -d --name n8n -p 5678:5678 -v n8n_data:/home/node/.n8n docker.n8n.io/n8nio/n8n
docker cp docs/examples/n8n-edgegate-gate.json n8n:/tmp/wf.json
docker exec n8n n8n import:workflow --input=/tmp/wf.json
docker exec n8n n8n publish:workflow --id=edgegateM3gate01 # 'activate' in older n8n
docker restart n8n # webhooks register at bootRead the request body from $json.body — not $json
The single most expensive mistake here. n8n’s Webhook node wraps the incoming request as {headers, params, query, body}. A node reading $json.chatInput gets undefined, and the workflow then answers the wrong thing with HTTP 200 — no error anywhere. Capture a baseline in that state and you certify garbage, then gate against it forever.
const inp = $json.body ?? $json; // correct
if (!inp.chatInput) throw new Error('no chatInput in request body');Response shapes
With no response_text_path set, the runner peels the wrappers no-code platforms add and then looks for reply / text / output / message / content / response. Verified against self-hosted n8n 2.33.6:
| Respond-to-Webhook mode | Body | Works unconfigured? |
|---|---|---|
| All incoming items (default) | [{"output": "..."}] | yes — 1-item list unwrapped |
| First entry JSON | {"output": "..."} | yes |
| Item envelope kept | [{"json": {"output": "..."}}] | yes — unwrapped |
| Fanned out, several items | [{...}, {...}] | no — set response_text_path |
A multi-item response is deliberately not guessed at: picking the first item would silently gate the wrong branch. Note the array shape when writing response_tools_path — against the default mode it is 0.actions, not actions.
Test URL vs production URL
n8n exposes two. /webhook-test/<path> fires once, only after you click Execute workflow. /webhook/<path> is live but exists only while the workflow is active. A gate run sends one request per eval case, so on the test URL case 1 succeeds and the rest 404. Always activate and use /webhook/.
Tool / action tracking
To gate what the workflow did, not just what it said, return the invoked action names and point response_tools_path at them:
{"output": "Done.", "actions": ["lookup_order", "issue_refund"]}
// response_tools_path: "actions"forbidden_actions in your eval set are diffed against that list — a workflow that starts calling issue_refund where the baseline refused flips the gate RED. The text marker tool_call(name=...) is also honoured and unioned in, so either surface scores identically.
Troubleshooting
| Symptom | Cause |
|---|---|
| 422 http.endpoint_url is required | vendor: "http" without an http block |
| 422 http config must not carry credentials | Move the key to EDGEGATE_HTTP_API_KEY on the runner |
| could not locate reply text in webhook response | Set response_text_path (dotted, e.g. result.data.0.answer) |
| capture_all_empty | response_text_path points at nothing — check against a real response body |
| reference eval_set_sha256 != ... | Reference captured against a different eval-set version — re-capture |
| reference system_prompt != run system_prompt | Same, for the prompt — re-capture under the prompt you will run |
Gate the workflow, not just the model
Create an endpoint from the Behavioral Gate tab in your workspace, or follow the steps above from CI.
CI/CD integration guide →© 2026 EdgeGate. Powered by Qualcomm AI Hub.