# n8n + AnswerUp Core: local synthetic record update

Application source and workflow JSON for independently accepted integration commit `cf5c3977e061d59558890a367fe4807110adbe3e`. The seven `src/*.mjs` files and both workflow files are unchanged accepted bytes. This README, package descriptor and verification summary are sanitized documentation additions. No SDK implementation or dependency archive is supplied.

Tested environment: Linux/WSL2, Node.js 22.23.2, npm 10.9.8, n8n 2.0.3, Manual Trigger 1, HTTP Request 4.2. No external business API, paid model or proprietary credential is required. The API, records, demo bearer values and fault controls are local synthetic fixtures.

## Save and install

Save `package.json`, this README, `verification.json`, all seven source modules and the two workflows, preserving these paths:

```text
src/action.mjs  src/host.mjs  src/provider.mjs  src/service.mjs
src/http.mjs    src/storage.mjs  src/cli.mjs
workflows/before-core-direct-http.json
workflows/governed-record-update.json
```

Obtain the selected SDK archives through your existing approved private-preview handoff. Package access and authorization are unchanged by this example. Do not expect public npm publication or public SDK archive URLs. Verify these exact SHA-256 values before installation:

```text
ba20e2b71fd0336505b01c63664c4aade4f023bd77fd9108d773d8bb2546d62b  answerup-core-1.0.0-rc.1.tgz
3d1f805f30199c498653a5cdd69d0b9df7bd2563ac79f87bdcb8d2ddda8a9336  answerup-runtime-0.2.0-preview.5.tgz
2f0023f4ada2086e4d52f52149ac9061c5d1f99d4eda82c22ae88f7b46aebc30  answerup-actions-0.2.0-preview.8.tgz
```

From your saved example directory, replace `/path/to/approved` with the location of your authorized archives:

```sh
npm install --ignore-scripts /path/to/approved/answerup-core-1.0.0-rc.1.tgz /path/to/approved/answerup-runtime-0.2.0-preview.5.tgz /path/to/approved/answerup-actions-0.2.0-preview.8.tgz
npm install --prefix tools/n8n --save-exact --no-audit --no-fund n8n@2.0.3
```

n8n installation needs public npm access and its native/transitive dependencies. Docker is not required. The source follows the selected Generic HTTP reference 0.2.0-preview.4; its identity is included in `verification.json`, but its private archive is not needed to run these saved application modules.

## Run the tested CLI path

Start `npm run provider` in terminal A and `npm start` in terminal B. They bind only to `127.0.0.1:4319` and `127.0.0.1:4318` and persist synthetic state under `.data/`. Optional service/provider environment: `PORT`, `STATE_FILE`; service only: `PROVIDER_URL=http://127.0.0.1:4319`. Stop gracefully with Ctrl-C. A state file allows one writer; abrupt interruption may require operator review, with no automatic stale-lock takeover.

In terminal C:

```sh
export N8N_USER_FOLDER="$PWD/.n8n"
export N8N_ENCRYPTION_KEY=answerup-local-synthetic-preview-only
export N8N_DIAGNOSTICS_ENABLED=false
export N8N_VERSION_NOTIFICATIONS_ENABLED=false
export N8N_TEMPLATES_ENABLED=false
export N8N_PERSONALIZATION_ENABLED=false
export N8N_COMMUNITY_PACKAGES_ENABLED=false
tools/n8n/node_modules/.bin/n8n import:workflow --input=workflows/before-core-direct-http.json
tools/n8n/node_modules/.bin/n8n execute --id=AUN8nDirect --rawOutput
tools/n8n/node_modules/.bin/n8n import:workflow --input=workflows/governed-record-update.json
tools/n8n/node_modules/.bin/n8n execute --id=AUCoreN8nPreview --rawOutput
```

Both files are inactive manual workflows. The before-Core node posts to `/direct-update`; the governed node posts to `/actions/update` with `Authorization: Bearer demo-operator`. The same body shape is `{requestId,recordId,label}`. No n8n credential object is required. `demo-viewer` is an authenticated but policy-denied local identity.

The governed workflow sets fixtures, makes a verified update, checks replay and supported/unsupported claims, then loses the response to a second update while hiding its authoritative read. Expected sequence: **COMMITTED → UNKNOWN → UNKNOWN → COMMITTED**, with no ordinary completion claim while UNKNOWN. It reveals the original evidence before the final verifier-only recovery. On a fresh direct+governed run: three provider writes/mutations, four effect reads, zero recovery redispatches; the lost-response operation has one write.

n8n 2.0.3's CLI JSON retains an early `status:running` snapshot with `finished:true`; the accepted verification also checked its final SQLite execution rows for successful completion and inspected every node output. The sanitized summary distinguishes those checks. A missing optional Python runner warning did not affect the built-in HTTP/Manual nodes.

## Read the result at the right boundary

| Observation | What it establishes |
| --- | --- |
| n8n node/execution success | The workflow engine completed its nodes and received responses. A successful HTTP node can contain an `UNKNOWN` action result. |
| Provider acknowledgment | The writer's observation, not proof of the business effect. |
| `COMMITTED` / `effectTruth.status: PRESENT` | Core classified independent verifier evidence for the exact historical effect. In this example, the host reads `/effects/:effectIdentity` and checks the original binding. |
| SDK `claims.decision: PERMITTED` plus the host's claim gate | A bounded statement about that effect, with matching identities and saved claim plan. The host returns `claimPermission: PERMITTED` only after those checks; effect truth alone is insufficient. |
| Logs or a Trace import | Retained observations for inspection, not new effect truth, live recovery authority or permission to retry. |

n8n orders HTTP calls and schedules the reconciliation request. The developer-owned service supplies trusted identity, policy, provider/verifier I/O, durable recovery authority and wording; Actions, Runtime and Core supply the governed contracts. Read [provider/verifier responsibilities](/docs/provider-verifier), [safe claims](/docs/safe-claims) and [the Actions API](/docs/api/actions#api-ProtectedActionRuntime).

## Optional readable logs and Trace capture

The downloaded service does **not** automatically write result logs or export Trace JSON. The following are optional observation points for a developer's own copy, not changes already present in the accepted source. Keep the existing admission, binding checks, recovery commit, claim projection and save ordering. A diagnostics failure must not become permission to resend an update.

### Write `.log` and `.jsonl` observations

Save the existing [host-owned diagnostics helper](/examples/optional-file-logging/src/diagnostics.mjs) as `src/diagnostics.mjs`; its [companion README](/examples/optional-file-logging/README.md) explains its schema and privacy choices. Initialize it once in the service process:

```js
import { createFileLogger } from './diagnostics.mjs';
const logger = await createFileLogger('.observations');
```

In `src/host.mjs`, `submit()` receives the live Actions result from `await runtime.run(...)`. After its existing `entry.latest = project(result, entry); await save();` and before returning, an optional host observation is:

```js
await logger.record({
  result,
  phase: 'INITIAL',
  customerConfirmationEmitted: entry.latest.claimPermission === 'PERMITTED',
  providerPostCount: state.counters.providerDispatches,
});
```

For `reconcile()`, use the same call with **`result: recovered.result`** and **`phase: 'RECONCILED'`**, after the existing accepted recovery commit, identity checks, `project(recovered.result, entry)` and save. `capability.reconcile()` returns a wrapper; `recovered.result` is the Actions result. The host's replay branches return a saved response without another SDK run; they are not new result boundaries.

The helper appends the same selected observations to `.observations/execution.log` (readable lines) and `.observations/execution.jsonl` (one structured object per line). It records result kind, lifecycle, effect truth, claim enums, historical dispatch flag and per-run HMAC identity aliases. The confirmation field reports the host's message decision, not external customer delivery. Here `providerPostCount` is the host's **cumulative dispatch-attempt count across both governed actions**; it is not an independently measured mutation count or a per-effect count. Compare it with provider events. Keep raw requests, full results, `/evidence`, `/state`, credentials and provider payloads out of these log files. The two appends are not an atomic recovery store.

### Capture live results for Trace Local

If your approved handoff includes **Trace Local `0.1.0-preview.7`**, unpack its archive; it is not an npm package. Its `trace-capture.mjs` and `TRACE-LOCAL-IMPORT.md` are supplied privately, not by a new public SDK export. Copy the helper next to the host module in your own instrumented copy. At the same two result boundaries, select the live package fields:

```js
import { actionsSnapshot } from './trace-capture.mjs';
// At the initial result boundary in submit():
const initialSnapshot = actionsSnapshot(result, 'INITIAL');
// At each accepted recovery result boundary in reconcile():
const recoverySnapshot = actionsSnapshot(recovered.result, 'RECONCILED');
```

These two calls belong in separate existing scopes. Retain their outputs in a bounded host collector **during execution**, grouped by original effect identity. Saving these helper outputs as JSON lines for later assembly is fine; deriving package snapshots from `.log` lines, n8n responses or saved full-result histories is not equivalent to live capture. Do not pass an Actions result to `runtime.project()`. Actions preview.8 normally returns an empty `answerup-actions-trace/1` envelope; zero records do not mean no dispatch or verification occurred. No `TraceSink.append` implementation or automatic SDK logging is supplied here.

Assemble one file per attempted effect. In this illustration, `snapshotsForThisEffect` is **your retained array**, not an SDK API: one `INITIAL` snapshot and up to three `RECONCILED` snapshots for the same operation/effect/execution. Attempt identities may differ. The null/empty host fields explicitly mean evidence was not retained:

```js
import { mkdir, writeFile } from 'node:fs/promises';
await mkdir('.observations', { recursive: true });
await writeFile('.observations/trace-effect.json', JSON.stringify({
  contract: 'answerup-local-trace/1',
  packages: { core: '1.0.0-rc.1', runtime: '0.2.0-preview.5', actions: '0.2.0-preview.8' },
  runRef: 'local:record-update', // Use your own opaque run reference.
  actionLabel: 'Synthetic record label update',
  results: snapshotsForThisEffect,
  host: {
    admission: null, authorization: null, dispatches: [], verifierReads: [],
    claimPlan: null, claim: null,
  },
}, null, 2), { mode: 0o600 });
```

To inspect the verifier and claim joins, replace those empty host fields only with actual bounded observations, following `TRACE-LOCAL-IMPORT.md`:

- Admission/authorization: the saved host admission and executed policy decision, with opaque references. Provider dispatch: the observed boundary/acknowledgment and original effect/attempt identities, not an inference from a recovered result flag.
- Verifier reads: in `src/action.mjs`, retain each actual `verify()` read after its HTTP check and before returning: `{phase, effectIdentity, status, observationRef, finalityRef}`. Use `INITIAL` or `RECONCILIATION`, the actual `PRESENT` or `INDETERMINATE` decision, and the returned `effect-journal-read:...` reference. Include `finalityRef: null` here; this example does not establish final absence. A counter alone cannot recreate a read observation.
- Claim plan/message: retain the saved plan's committed proposition and matching effect/execution identities, and the host's emitted/withheld message decision. Use the helper's `opaqueRef()` consistently for the free-text proposition in both `host.claimPlan` and `host.claim`; use a redacted wording template, not customer text.

All six host keys must remain present. Missing reports remain null/empty and limit the alignment assessment. Follow the bundled contract's exact nested fields, reference rules, redaction requirements and 64 KiB limit; do not combine the verified and lost-response effects in one history. The [Portal Trace documentation](/docs/trace) explains the evidence boundary.

### Open and inspect the captured file

From the unpacked selected Trace directory:

```sh
cd dist
python3 -m http.server 5191 --bind 127.0.0.1
```

Open `http://127.0.0.1:5191/`, choose **Load evidence JSON** and select your captured file. Compare the displayed SHA-256 with `sha256sum .observations/trace-effect.json` run from the companion directory. Use the execution overview to compare initial UNKNOWN, later reconciliation, final effect truth and final-only claim permission. Select an early result row before opening **Separate · final supplied state**; later PERMITTED is not permission at the earlier UNKNOWN boundary. Expand **Verification detail** / **Technical explanation** and the host-reported reads to inspect joins or missing evidence. The timeline groups supplied evidence rather than reconstructing wall-clock order. An alignment message does not authenticate host reports; the file stays in the local browser. Built-in examples are synthetic and are not your execution.

## Try the graphical editor — execution still unverified

The accepted baseline validated CLI execution. Independent DX-3 also completed the 14-node governed CLI workflow and opened the editor at owner-account setup; it did **not** complete owner setup, import through the editor or execute there. The steps below describe the intended local path, not a newly validated editor result.

1. Keep the provider and companion service running on the same machine/network namespace as n8n: the workflow's `127.0.0.1` URLs refer to that machine. Stop any n8n CLI execution before starting the editor. In a terminal with the same `N8N_USER_FOLDER`, encryption key and other exports above, run:

   ```sh
   N8N_PORT=5683 N8N_LISTEN_ADDRESS=127.0.0.1 tools/n8n/node_modules/.bin/n8n start
   ```

2. Open `http://localhost:5683/`. For a fresh instance, complete n8n's owner signup with email, first/last name and a password of at least eight characters including a number and capital letter. This is a local n8n account, separate from AnswerUp Portal access. See [n8n user-management setup](https://docs.n8n.io/hosting/configuration/user-management-self-hosted/).
3. Open the workflow already imported by CLI, or create a workflow and choose **Import from File** in the workflow menu, selecting `workflows/governed-record-update.json`. See [n8n import instructions](https://docs.n8n.io/workflows/export-import/). Keep the manual workflow inactive; no AnswerUp credential object is required for these synthetic demo bearer values.
4. Inspect the nodes, then use **Execute workflow** for a complete [manual execution](https://docs.n8n.io/workflows/executions/manual-partial-and-production-executions/), without pinned data. Inspect the four result nodes and provider/host evidence. Do not treat step reruns or a new `$execution.id` as recovery of an uncertain business action; reconcile the original request instead. Editor import, canvas execution and node inspection remain unverified for this example.

## CLI messages and historical flags

- **`Could not find workflow` during import:** DX-3 observed an initial webhook-cleanup warning followed by `Successfully imported 1 workflow.` and a later successful execution. Check the final import result and workflow ID; do not assume every occurrence of this message is harmless.
- **Missing Python task-runner environment:** the tested Manual Trigger/HTTP Request workflows used no Python node and completed despite this warning. It does not establish that Python workflows work.
- **Task Broker port 5679 already in use:** an editor and CLI instance can contend for the default broker port. This prevented a DX-3 CLI attempt from starting; stop the competing instance before running the documented CLI path. It is not an SDK result or evidence of a provider write.
- **Raw `status: running` with `finished: true`:** inspect final execution status and node outputs rather than that early CLI snapshot alone. With the documented user folder, the local SQLite database is `.n8n/.n8n/database.sqlite`; the read-only query `SELECT id, status, finished, workflowId FROM execution_entity ORDER BY id DESC LIMIT 5;` shows the terminal rows. Successful n8n status still does not imply a verified business effect.
- **`providerDispatchOccurred: true` on a recovered result:** this preserves the historical dispatch distinction for the original attempted effect. It does not say that reconciliation made another write. Compare original effect/attempt identities, actual provider write events, verifier reads and cumulative counters. In DX-3, the governed workflow made two writes and four independent reads; the uncertain effect had one write and three reads, with zero recovery redispatches. The existing direct-plus-governed baseline totals above include a third, direct write.

## Identity, recovery and claims

Workflow `$execution.id` values identify new synthetic operations per run. In a real workflow, use the original durable business operation ID across retries. The same actor/request/input replays its saved result; changed input under the same request ID conflicts. An UNKNOWN result is never permission to make another write.

Host endpoints: `POST /actions/update`, `POST /actions/:requestId/reconcile` with `{}`, `POST /claims/check` with `{requestId,requestedClaim}` and authenticated `GET /evidence`. The synthetic provider exposes `/records/:recordId`, write `/updates`, authoritative historical `/effects/:effectIdentity`, `/fixtures` and `/state`. Fixture and full-state endpoints are local development tools, not a production API design or a safe production logging template.

To reproduce a graceful restart, stop the service after the `Lost response UNKNOWN` node has completed and persisted its accepted recovery export. Restart `npm start` against the same state file. Reconcile the original request with an empty body. If readback is hidden it remains UNKNOWN. Reveal it using `POST :4319/fixtures` with `{"recordId":"record-lost","visible":true}`, then reconcile again. Recovery reads the original effect; it never redispatches the update. Restart behavior was verified separately from the continuous n8n CLI run.

`record_updated` is permitted only for the exact historical update with matching effect and claim permission. `email_sent`, another record, current state after later changes and whole-workflow completion are unsupported. The accepted negative test established COMMITTED/PRESENT truth while withholding a wrong-subject claim. JSON histories and saved responses are not live SDK authority.

Unverified: **interactive-editor execution, arbitrary-crash recovery, distributed recovery and production provider behavior**. Other n8n versions, general exactly-once guarantees and production readiness are also unestablished. See the Portal's provider/verifier, UNKNOWN reconciliation, safe-claims and Actions API documentation before adapting this local example.
