# Outstand comment-to-DM n8n workflow

Import `comment-to-dm-n8n.json` into n8n. It is inactive and contains no credentials. Configure everything below before publishing. This is an automation template, not a signature gateway or an exactly-once delivery system.

## Prerequisites

- A current n8n installation supporting Webhook v2, Code v2, Postgres v2.6, Respond to Webhook v1.4, If v2.2 and HTTP Request v4.2.
- A connected Instagram professional account or Facebook Page, appropriate Meta permissions, and an Outstand API key with `conversations:send` permission. Facebook requires `pages_messaging`.
- A separate persistent PostgreSQL database or schema. Do not change n8n's internal tables. Use TLS and a least-privilege database credential.
- An HTTPS signature-verifying gateway you operate. It must preserve raw request bytes, verify `X-Outstand-Signature` as `sha256=<hex HMAC-SHA256>` with the Outstand webhook endpoint secret using a constant-time comparison, reject missing/invalid signatures, and only then parse and forward the JSON. Set a payload-size limit, strip incoming `X-Verified-Gateway`, and inject your own random shared secret in that header. Do not expose the forwarding secret to browsers or put it in JSON exports. A header saying "verified" without cryptographic verification is not sufficient.
- The gateway must return success upstream only after n8n responds 202. Transient failures before acknowledgement may redeliver the same event with bounded backoff; never implement retries of the conversation send at the gateway. Configure its upstream timeout above the normal claim latency. Keep its forwarding endpoint fixed, authenticated and HTTPS. Restrict direct webhook ingress where possible. The workflow itself does **not** verify Outstand signatures.

## Database setup

Run once as a database administrator, then grant the workflow role only schema usage and SELECT/INSERT/UPDATE on this table:

```sql
CREATE TABLE public.outstand_comment_dm_ledger (
	dedup_key text PRIMARY KEY,
	payload jsonb NOT NULL,
	status text NOT NULL CHECK (status IN ('claimed', 'pending', 'sent', 'needs_review')),
	http_status integer,
	conversation_id text,
	message_id text,
	created_at timestamptz NOT NULL DEFAULT now(),
	updated_at timestamptz NOT NULL DEFAULT now()
);
```

Select the same Postgres credential on both database nodes. Atomic INSERT ON CONFLICT admits one attempt across concurrent executions. Keys include organization, network, account and comment, not a campaign version. Keep keys for as long as replay is possible. Deleting rows removes replay protection; back up the ledger. Payloads contain the DM and recipient identifiers: apply retention/access controls accordingly.

## Configure credentials and campaign

1. On **Verified gateway only**, select a Header Auth credential with name `X-Verified-Gateway` and the gateway's random secret as value. This is not the Outstand signing secret.
2. On **Send private reply**, select a separate Header Auth credential with name `Authorization` and value `Bearer YOUR_OUTSTAND_API_KEY`. Never paste a real key into node parameters or committed files.
3. In **Scope and build payload**, replace `orgId`, each target's account ID and **native** `platformPostId`, the keyword and the message URL. Remove unused targets. `accountId` is the Outstand account ID; `platformPostId` is not Outstand's `postId`. Copy the values from a genuine verified `comment.received` event. Unmanaged posts may have `postId: null` while `platformPostId` is available.
4. Only `instagram` and `facebook` top-level, non-live feed comments are supported. Other events, replies, missing IDs, wrong organizations, wrong posts/accounts and non-keyword text are ignored. Instagram needs `authorId`. Keyword matching is case-insensitive, NFKC-normalized whole-token matching; `GUIDE!` matches `guide`, `guidebook` does not. It is not a multiword or fuzzy matcher.
5. Enable the Outstand `comment.received` subscription and point it to your **gateway**, not directly to n8n. Publish the workflow and give the gateway n8n's production webhook URL, not its temporary test URL.

## Requests sent

Instagram:

```json
{
	"social_account_id": "account-id",
	"content": "Your requested link",
	"instagram": { "comment_id": "comment-id", "author_id": "author-id" }
}
```

Facebook:

```json
{ "social_account_id": "account-id", "content": "Your requested link", "facebook": { "comment_id": "comment-id" } }
```

Both use `POST https://api.outstand.so/v1/conversations`. One private reply is allowed per comment, within seven days. Additional messaging requires the person to reply and is subject to the messaging window. A keyword comment does not grant permission for an unlimited DM sequence.

## Failure handling and operations

The 202 acknowledgement means an eligible event was durably **claimed**, or ignored/already claimed. It does not mean a DM was sent. PostgreSQL must succeed before acknowledgement. Claim failures stop execution. No message is sent without a new claim.

The HTTP node has a 15-second timeout, redirects disabled and retries disabled. HTTP non-2xx and transport errors reach classification; only a successful response with conversation/message IDs and `message.status: sent` records `sent`. `pending` stays `pending`; timeouts, 409s, 429s, 5xx, unexpected responses and other failures become `needs_review`. There is no automatic resend, even for a seemingly transient response. The workflow has a 120-second execution timeout. A database failure or process crash after the send can leave `claimed`; that is ambiguous, not safe to replay.

Use saved n8n execution data and the ledger to investigate. Configure an n8n Error Workflow for executions that actually fail, and separately monitor `needs_review`, old `claimed` and old `pending` rows, because handled errors can finish with a green execution. Example:

```sql
SELECT dedup_key, status, http_status, conversation_id, message_id, updated_at
FROM public.outstand_comment_dm_ledger
WHERE status = 'needs_review'
	OR (status IN ('claimed', 'pending') AND updated_at < now() - interval '5 minutes');
```

Reconcile IDs against Outstand's conversation/message records and delivery events before manual action. Never retry the HTTP node in isolation, delete a claim and replay, or assume a timeout means no message was accepted. No scheduled recovery worker is included: a crash after claim but before send can drop an automatic send. This deliberately favors avoiding duplicates over guaranteed delivery. Execution progress is saved, but is not an exactly-once transaction spanning PostgreSQL and Outstand. Set n8n pruning, backups and access policy explicitly; saved executions can contain personal data. Successful execution persistence is not permanent archival.

Public confirmation is intentionally absent. Add one only on a verified `sent` path, or after separately correlating `message.sent` for a pending message. `success: true`, a 202 webhook response and a `pending` status are not proof of delivery. A confirmation also needs its own deduplication and failure policy.

## Test before use

### Validation performed for this download

- Imported and exported successfully using the actual `n8nio/n8n:2.47.5` runtime. The round trip preserved all nine nodes, inactive state and execution-persistence settings.
- Checked every configured node version and parameter name, including nested collection parameters, against the node descriptions installed in that runtime.
- Executed the exported Code-node JavaScript with fixture inputs: fail-closed defaults, exact Instagram/Facebook request shapes, organization/account/post/network restrictions, missing identifiers, non-keyword text, live/reply exclusions, Unicode normalization, sent/pending classification, and rejected/ambiguous response classification.
- Executed n8n's real Postgres `executeQuery` operation against disposable PostgreSQL 17 using the README DDL and workflow SQL. Twenty concurrent attempts admitted exactly one claim. Ineligible events inserted nothing. JSON, quotes and Unicode survived parameter binding, and outcome updates preserved null fields.

These are import, installed-schema, JavaScript-fixture and real database-node tests, not a full activated webhook-to-HTTP run. No production gateway, signature verifier, live Outstand send, Meta delivery or public confirmation was exercised. Runtime credential selection, gateway verification, monitoring and a controlled live account test remain operator prerequisites.

Test invalid gateway authentication, unsupported networks, account/post mismatches, missing Instagram author ID, keyword/non-keyword text, and duplicate/concurrent deliveries. Confirm PostgreSQL prevents a second claim. Exercise sent, pending, rejected and timeout responses without contacting real recipients. Then run a controlled live test on your own connected account with a consenting commenter. Local validation does not prove Meta permissions or live delivery.

Sources: [n8n Postgres](https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.postgres/), [Webhook](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.webhook/), [HTTP Request](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.httprequest/), [Outstand docs](https://www.outstand.so/docs).
