---
title: "n8n Sandbox Environment"
slug: n8n-sandbox-environment
section: Integrations
status: stable
description: "The n8n sandbox is an isolated n8n instance with mocks of the Globus endpoints n8n calls back into, plus a fixture-driven integration suite that proves the Dispatcher and domain handlers work with no real Globus environment reachable."
last-updated: 2026-10-07
---

# n8n Sandbox Environment

The n8n sandbox is an isolated n8n instance with mocks of the Globus endpoints n8n calls back into, plus a fixture-driven integration suite that proves the Dispatcher and domain handlers work with no real Globus environment reachable.

## Overview

The sandbox instance (namespace `n8n-sandbox`, reachable at `sandbox.n8n.allianceabroad.com`) is separate from the `development` to `qa` to `main` promotion chain and is not a deploy target; workflows are exported from it one way and promoted into `development` by an explicit action. Its topology, branch and promotion tooling are described on [Automation (n8n)](/docs/automation) and [Git Branching & Release Model](/docs/git-branching-release-model). This page covers the test harness built on top of it: the contract bundle, the Contract Harness mock, the tracking tables, the isolation guards and the integration fixtures. [1] [2]

The harness was delivered in phases (contract bundle, tracking tables plus mock, outbound simulator, rewiring, acceptance and finance fixtures) through n8n-workflows PRs 297, 299, 303, 305, 307 and 309. [3]

## Architecture

| Piece | Role | Location |
| --- | --- | --- |
| Contract bundle | Schemas and manifest describing the Globus and n8n wire contract; sole input for the mock and the simulator | `contracts/globus/v1/` |
| Outbound simulator | `scripts/test_integration.py` fires the real Globus to n8n envelope at the Dispatcher webhook | `scripts/test_integration.py` |
| Dispatcher and handlers | The real sandbox copies of the Dispatcher and domain handlers under test (for example the Applicant and Reference handlers) | `workflows/sandbox/` |
| Contract Harness | n8n workflow named "[Sandbox] Contract Harness: Globus Inbound Mock" that answers the finance and event-callback calls n8n makes into Globus; a separate SendGrid Delivery Mock covers the branded delivery pipeline | `workflows/sandbox/KioStIAoroEpos0b__sandbox-contract-harness-globus-inbound-mock.json` |
| Tracking tables | Sandbox Postgres tables holding correlation and origin evidence | `scripts/sql/phase1_sandbox_tracking_tables.sql` |

Flow for one fixture: with `PG_USER` and `PG_PASSWORD` set, the runner issues an `eventId` from `sandbox_test_events`, posts the envelope with `callbackUrl` pointed at the Contract Harness, waits for the Dispatcher ack (HTTP 202), finds the handler execution, asserts on its result, and, for fixtures that verify the callback, polls the tracking table to confirm the Dispatcher's completion callback reached the mock. [4] [5]

## Contract Bundle

Bundle `globus-n8n-contract-bundle` version `1.0.0` lives in `contracts/globus/v1/`. It was hand-assembled from `BackOfficePrototype` source at a recorded commit, not emitted by a generator, and is intended to be superseded by a Globus-owned generated bundle. The mock and simulator must not read `BackOfficePrototype` source directly; the bundle is the boundary. [6]

| Contract | Direction | Route |
| --- | --- | --- |
| Outbound dispatcher envelope | Globus to n8n | Dispatcher webhook, `X-N8N-API-KEY` |
| Event callback | n8n to Globus | `POST /api/n8n/event-callback` |
| NetSuite invoice sync | n8n to Globus | `POST /api/integrations/n8n/netsuite/invoice-sync` |
| Finance notification claim | n8n to Globus | `POST /api/integrations/n8n/finance/notification-deliveries/claim` |
| Finance notification complete | n8n to Globus | `POST /api/integrations/n8n/finance/notification-deliveries/:deliveryId/complete` |
| Applicant finance link | n8n to Globus | `PUT /api/integrations/applicants/:applicantId/finance-link` |

The manifest also defines further contracts not tabulated here: the SendGrid delivery claim, instruction, acceptance and dispatch-result endpoints, template-admin actions, the SendGrid webhook ingress, email-template operations and application domain events. See `contracts/globus/v1/manifest.json` for the full list. The finance provider-events spec (`provider-events-v1`) was retired on 2026-09-14 and superseded by the SendGrid webhook ingress contract (`POST /api/webhooks/sendgrid/events`, signature-verified in Globus). [6]

Contract facts the mock reproduces: [6]

- The envelope carries `eventId`, `id` (always equal to `eventId`), `eventType`, `timestamp`, `data`, and an optional `callbackUrl`. Fields such as `domain`, `payload` and `receivedAt` are Dispatcher-internal and are not part of the wire contract.
- The event callback is a `POST`. An unknown `eventId` is an idempotent 200 acknowledgement, not a 404.
- A claim that finds an existing delivery returns 200 with `claimed` false; the workflow must branch on `claimed` and skip sending.
- A completion with outcome `FAILED` is persisted and then answered with a 502 error envelope (code `NOTIFICATION_DELIVERY_FAILED`) so n8n's error branch fires. It is not a 200 with a failed state.
- Transport authentication for the Dispatcher is the direct `X-N8N-API-KEY` route only; the HMAC `X-Globus-Signature` fan-out is excluded from the n8n managed subscription.

> **Deployment parity gate**
>
> The bundle manifest records the webhook-auth decision as confirmed at `main` source level only; parity with the deployed dev and QA build is marked open. [6]

## Contract Harness

The Contract Harness is one n8n workflow with a header-auth webhook per mocked finance and event-callback endpoint. The runner also relies on a separate SendGrid Delivery Mock (seed, claim, instruction, acceptance and dispatch-result webhooks) and a SendGrid Activity Lookup helper, which are not covered in this section. Each endpoint validates the request shape, reads or writes minimal correlation state in Postgres, and answers in the contract's envelope. It does not reimplement Globus business logic. [5] [7]

| Mock endpoint | State it keeps | Notable behavior |
| --- | --- | --- |
| Event callback | `sandbox_test_events` status and `callback_origin` | Unknown event returns the idempotent 200 ack; bad body returns 400 |
| Invoice sync | `sandbox_finance_invoices`, upserted on `external_ref` | Negative amounts or `amountDue` above `amountTotal` return 422 `POLICY_VIOLATION`; returns the generated invoice id as `invoice.id` |
| Notification claim | `sandbox_finance_deliveries`, insert-or-skip on `delivery_key` | Unknown invoice returns 404; repeat claim returns `claimed` false with the same delivery |
| Notification complete | Updates delivery `state` | `SENT` returns 200; `FAILED` returns 502; already `SENT` returns 409; unknown delivery returns 404 |
| Finance link | `sandbox_applicant_finance_links`, upsert on `applicant_id` | Returns `created` from the insert flag |
| Provider events (legacy) | `sandbox_provider_events`, unique on delivery and `sg_event_id` | Leftover finance-scoped mock; duplicate event returns `replayed` true |

Because n8n did not register dynamic path segments for API-created webhooks, the mocks for complete, finance-link and provider-events take the id as a query parameter (`?deliveryId=` or `?applicantId=`) instead of a path segment. Workflow nodes that call the harness must build their URLs that way. [7]

## Sandbox Tracking Tables

Created in the sandbox Postgres by `scripts/sql/phase1_sandbox_tracking_tables.sql`, fully decoupled from Globus `ops_events`. [8]

| Table | Purpose |
| --- | --- |
| `sandbox_test_events` | One row per fired event; `status` moves from `dispatched` to `completed` or `failed` (the column comment also lists `unknown`, which the mock does not write); `callback_origin` records the host the callback used |
| `sandbox_finance_invoices` | Invoice correlation keyed by unique `external_ref`; records `sync_origin` |
| `sandbox_finance_deliveries` | Delivery correlation keyed by unique `delivery_key`; `state` is `CLAIMED`, `SENT` or `FAILED`; records claim and complete origins |
| `sandbox_applicant_finance_links` | Applicant to NetSuite customer link |
| `sandbox_provider_events` | Dedup of provider events on delivery and `sg_event_id` |

The SQL file describes a 30-day retention delete keyed on `created_at`, but states it is not scheduled; run it manually if needed. [8]

## Isolation Guards

The goal is that nothing in the sandbox can reach a real Globus deployment. PR 305 applied these changes to the sandbox Dispatcher, Finance handler and Reference handler; later PRs in the series built on them. [9] [10] [11]

- The Dispatcher performs the single completion callback for every domain. The Reference handler's own drifted callback node (wrong method, no credential, swallowed failures) was deleted.
- Callback failures are no longer swallowed, so a failed callback fails the Dispatcher execution. The crash-recovery path ends in a local alert node instead of retrying the callback, which prevents recursion.
- An exact-origin allowlist guard on the Dispatcher and on all three Finance callback call-sites rejects, with no network request, any callback whose origin is not the Contract Harness origin (PR 305).
- PR 305 also moved the Dispatcher's callback nodes to a dedicated static sandbox-only credential rather than substring-based environment selection.
- Hardcoded and fallback real-dev-Globus URLs were removed. This included the Persist Globus Finance Link callback in the NetSuite Customer Ensure sub-workflow, which read an n8n variable that is unavailable on this instance and so always fell through to the real dev Globus fallback URL; it now reads the environment variable with no fallback, passes the same exact-origin guard, uses the sandbox-only credential and the query-parameter URL form.
- The runner always sets `callbackUrl` to the Contract Harness and never relies on a Dispatcher fallback. [4]

Runtime evidence for the release gate is the origin columns in the tracking tables, which the finance fixtures assert equal `sandbox.n8n.allianceabroad.com` for the invoice-sync and claim origins; the event callback origin is recorded and reported. [5]

> **Not every workflow is guarded**
>
> PR 309 reports that a sweep of the 37 sandbox-mapped workflows found the same unprotected callback pattern in 13 workflows outside the finance fixture path, tracked as a separate follow-up. [11]

## Integration Test Runner

`scripts/test_integration.py` supports `--env sandbox` only; other environments exit with an error. It posts to `/webhook/globus-events` with `X-N8N-API-KEY`, expects a 202 ack, then locates the handler execution through the n8n REST API and asserts on the handler's result node. Fixtures that set `verify_callback` also poll `sandbox_test_events` until the status leaves `dispatched` and require `completed`. Without `PG_USER` and `PG_PASSWORD`, callback-verifying fixtures fail closed rather than skip. The runner also supports `--filter` to run fixtures by name substring. Before firing each fixture that targets a handler, it records the highest existing execution id of that handler, and the execution wait ignores any execution at or below that id, so back-to-back fixtures on the same handler cannot match a previous run. [5]

## Fixture Matrix

At `development` HEAD the suite contains these fixtures plus one adapter check. [5]

| Fixture | Handler | Dependency policy |
| --- | --- | --- |
| Control `program.created` | None (Dispatcher no-op) | None; the 202 ack is the assertion |
| `partner.reservation.created`, empty payload | Partner domain | Clean skip path, no external call |
| `compliance.became_compliant` | Compliance domain | Clean skip path, no external call |
| Acceptance A `applicant.created` | Applicant domain | HubSpot stubbed via the `.invalid` email skip; real NetSuite Sandbox contact upsert and finance-link callback to the mock |
| Acceptance A2 `applicant.invite_resent` | Applicant domain | Same branches as A |
| Acceptance A3 `external_notification.created` | External notification domain | Claim, instruction and dispatch pipeline against the mock's SendGrid delivery seed endpoint |
| Acceptance B `reference.invited` | Reference domain | Real SendGrid send, verified through the sandbox-only SendGrid Activity Lookup helper |
| Finance `invoice_requested` happy path | Finance invoice request | Real NetSuite Sandbox invoice, mock claim and completion, real SendGrid send; asserts origin evidence |
| Finance invalid payload (missing `lines`) | Finance invoice request | Expects the execution to fail with `INVALID_REQUEST:lines` |
| Finance duplicate (same `externalRef`) | Finance invoice request | Expects still exactly one delivery row, so no second send |
| Template-admin adapter check | Direct `globus-adapter/template-*` webhooks | Draft, fetch, activate, fetch round trip; creates one tagged SendGrid template per run |

The duplicate fixture must run after the happy-path fixture because they share an `externalRef`. [5]

## Running the Suite

Required and optional environment variables, names only: [5]

| Variable | Use |
| --- | --- |
| `N8N_HOST`, `N8N_API_KEY` | Sandbox n8n base URL and REST API key |
| `PG_USER`, `PG_PASSWORD` | Enable tracking-table event ids and callback verification; `PG_HOST`, `PG_PORT`, `PG_DATABASE` default to `127.0.0.1`, `15432`, `n8n` |
| `SANDBOX_GLOBUS_CALLBACK_KEY` | Sandbox-only `X-API-Key` for the Contract Harness, the SendGrid Activity helper and the mock seed endpoint |
| `SANDBOX_TEMPLATE_ADMIN_API_KEY` | Key for the template-admin adapter check; without it the check fails closed with a 403 |
| `CONTRACT_HARNESS_CALLBACK_URL`, `SENDGRID_ACTIVITY_HELPER_URL`, `MOCK_SENDGRID_SEED_URL` | URL overrides, defaulting to the `sandbox.n8n.allianceabroad.com` webhooks |
| `N8N_DISPATCHER_API_KEY` | Overrides the Dispatcher webhook key if it differs from `N8N_API_KEY` |

The suite is run manually through the `Test Sandbox` GitHub Action (`workflow_dispatch` only) or locally against a port-forward; it gates no branch. [12]

## Known Gotchas

- Several fixtures produce real external effects: NetSuite Sandbox invoices and contacts, real SendGrid sends, and a SendGrid template per run. Earlier documentation describing the suite as side-effect free is out of date. [5] [12]
- SendGrid's Activity API can take minutes to index a send, so the email verification poll window is 360 seconds. [5]
- A mock that does not match the real contract (for example normalizing the 502 on a failed completion into a 200) makes the failure branch untestable. [6]
- The harness still contains a legacy provider-events mock from the retired finance provider-events spec; it is not a live contract. [6]

## Sources

- n8n-workflows#297: contract bundle v1 (`contracts/globus/v1/`). [6]
- n8n-workflows#299: tracking tables and Contract Harness (`scripts/deploy_contract_harness*.py`, `scripts/sql/phase1_sandbox_tracking_tables.sql`). [7] [8]
- n8n-workflows#303: outbound simulator isolation in `scripts/test_integration.py`. [4]
- n8n-workflows#305: Dispatcher, Finance and Reference rewiring. [9]
- n8n-workflows#307: acceptance tests A and B, SendGrid Activity Lookup helper. [10]
- n8n-workflows#309: finance fixture matrix. [11]

## References

[1]: https://github.com/Alliance-Strategies/n8n-workflows/blob/development/README.md "n8n-workflows README, Sandbox tier"
[2]: /docs/automation "Automation (n8n)"
[3]: https://github.com/Alliance-Strategies/n8n-workflows/pulls "n8n-workflows pull requests 297, 299, 303, 305, 307, 309"
[4]: https://github.com/Alliance-Strategies/n8n-workflows/pull/303 "PR 303: Globus outbound simulator"
[5]: https://github.com/Alliance-Strategies/n8n-workflows/blob/development/scripts/test_integration.py "scripts/test_integration.py"
[6]: https://github.com/Alliance-Strategies/n8n-workflows/blob/development/contracts/globus/v1/manifest.json "Contract bundle manifest"
[7]: https://github.com/Alliance-Strategies/n8n-workflows/pull/299 "PR 299: tracking tables and Contract Harness"
[8]: https://github.com/Alliance-Strategies/n8n-workflows/pull/299 "phase1_sandbox_tracking_tables.sql"
[9]: https://github.com/Alliance-Strategies/n8n-workflows/pull/305 "PR 305: Phase 4 rewiring"
[10]: https://github.com/Alliance-Strategies/n8n-workflows/pull/307 "PR 307: acceptance tests A and B"
[11]: https://github.com/Alliance-Strategies/n8n-workflows/pull/309 "PR 309: finance fixture matrix"
[12]: https://github.com/Alliance-Strategies/n8n-workflows/blob/development/README.md "README, Integration Tests (Sandbox)"
