# Setup and operating guide

## 1. Prerequisites

Use an n8n installation supporting Schedule Trigger **1.4**, HTTP Request **4.5**, Manual Trigger **1**, and Error Trigger **1**. These are node type versions verified against the connected n8n catalog on 2026-10-10, not an invented n8n application release number. If your instance lacks them, upgrade/test against its installed node schemas before activation; do not silently downgrade JSON version numbers.

Deploy the included service using Python 3.12+ with Pillow or the provided Docker image. Keep **one service process per database**, on a private network accessible to n8n. Provide durable local storage, encrypted disks/backups and restricted operator access. SQLite supports this small-team deployment; it is not a high-availability database. Pin your tested base-image digest and dependency lock for your deployment after dependency/security review.

Required integration accounts:

- HubSpot private app with read permission for deals, deal schema/pipelines and owners. Common scope names include `crm.objects.deals.read`, `crm.schemas.deals.read`, `crm.objects.owners.read`; verify the exact scopes and account entitlements in your portal. No CRM write scopes are needed. Preflight every implemented endpoint with the actual token.
- Slack bot with `chat:write` and `files:write`, invited to every destination channel/DM. Use real C/G/D conversation IDs. Resolve recipient-to-DM IDs during setup; the adapter does not guess identities or open DMs from email addresses.
- SMTP provider supporting STARTTLS on port 587, authenticated sending, a verified sender and permitted recipients. Configure SPF/DKIM/DMARC with that provider. Preserve the generated Message-ID/References if a relay rewrites headers.
- Finance-approved FX rates in base-currency units per unit of source currency. Example EUR/GBP values are fictitious. Supply a rate for every source currency; refresh the rate date/table each week before the report. For single-base-currency portals no conversion is needed. Automating an approved FX feed is a separate upstream integration; no FX vendor API is invented here.
- Optional OpenAI API credential and a model supporting the configured structured-output JSON schema. AI disabled is fully supported.

## 2. Configure the workspace

Copy `config.example.json` to `config.json` and `.env.example` to `.env`. Keep both out of version control; `.env` must be accessible only to the service operator. Generate a random service token of at least 32 characters and set `DIGEST_SERVICE_TOKEN`. All environment values beginning with `REPLACE` are rejected when used.

Edit:

1. Workspace ID/name, base currency and IANA timezone. Tbilisi is **Asia/Tbilisi**, not Europe/Tbilisi.
2. `crm.single_currency`: null for genuine per-record currency; otherwise the portal's verified single currency. Never infer a missing currency from an amount.
3. Thresholds, page cap and maximum collection duration. Keep request execution budgets below n8n's 300-second HTTP timeout under expected load.
4. `fx.date`, `fx.rates`, `fx.source`: for a Monday October 12 run, the reporting week ends before October 12; a finance-approved Friday October 9 table is acceptable with the example four-day age tolerance.
5. Audience role/owner IDs and **verified destination permissions**. Managers have explicit owner allowlists, reps exactly one owner, executives all owners. The sample IDs and mailboxes are placeholders. Add one audience record per actual destination pair. Do not route rep reports to shared channels.
6. SMTP hostname/from address/message-ID domain; Slack admin channel; environment variable names for each secret.
7. Optional AI enabled/model. Business rules stay in config; credentials stay in the process environment or your deployment's secret manager.

The service loads config at startup; restart it after changes. Config is frozen for a claimed period. Changing config after a run starts causes that period to refuse reuse, preventing a half-old/half-new digest. Use a separate disposable preview database for setup. Do not change a production run's audience authorization underneath its delivery ledger.

## 3. Deploy the service

The provided compose file joins an existing Docker network. Set `N8N_NETWORK` to the network of your n8n container. Then:

```sh
docker compose build
docker compose up -d
```

No host port is exposed. n8n on that network uses `http://digest-service:8080`. On n8n Cloud or another host, deploy the service behind authenticated HTTPS and network restrictions, then replace the base URL in every HTTP node. Do not expose the raw HTTP port publicly. The service accepts only authenticated POST requests; it has no public report-download endpoint.

The named `digest-data` volume contains the SQLite DB and WAL. Back it up consistently using SQLite's backup API or with the service stopped. Do not delete/recreate it to retry a failed production run.

For local code verification without Docker:

```sh
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/python -m unittest discover -v -s tests -t .
.venv/bin/python -m tests.make_samples
```

## 4. Import and connect n8n

1. Import each of the three `.json` files through **Import from File**. Import JSON, not `.sdk.js`. The SDK files are companion source definitions used for validation. The JSON workflows are inactive; no published schedule is included.
2. Create the credential named **Digest service - configure Bearer token**. Use Templated Custom Auth with a template equivalent to `{"headers":{"Authorization":"Bearer {{api_key}}"}}` and store the service token as `api_key`. Select that credential in every HTTP Request node. No fabricated credential ID is embedded in the exports.
3. If your installed n8n supports a conventional Header Auth credential instead, explicitly change `genericAuthType` and credential binding on each HTTP node together; the header must be `Authorization` with value `Bearer <service-token>`. Do not paste secrets into node parameters.
4. Set the weekly workflow's timezone to exactly match `config.timezone`. Its cron is Monday 08:00. The service computes date windows independently using config. Update recovery timezone too for consistent operations display.
5. Save the error-handler workflow, publish/activate it if required by your n8n deployment, and select it under **Error workflow** in both weekly and recovery workflow settings. The export cannot contain its future instance-specific workflow ID. Do not point the handler at itself.
6. Keep weekly and recovery schedules unpublished during initial checks.

The suite uses a shared error workflow. An Error Trigger embedded in the main workflow is another n8n pattern on supporting versions; a separate handler here makes one operational route reusable. Both need production failure testing; manual tests do not prove Error Trigger delivery.

## 5. Dry run and enable sending

Keep `dry_run:true`. Use a separate preview data volume, actual read-only HubSpot credentials, correct FX data and private test destinations. Execute the weekly workflow manually. It should fetch, validate, store reports and render charts while skipping all Slack/email sends. Inspect the output status, ledger and exported scoped artifacts.

An operator can export prepared reports from the container to a mounted private directory, or locally from a copied database:

```sh
python -m service.manage --db /path/to/digest.sqlite3 status
python -m service.manage --db /path/to/digest.sqlite3 export 'acme:2026-10-05' /private/review-output
```

Verify canonical totals against a CRM export with the same filters and collection timing. Verify stage classification, currency availability, a deliberately empty scope, owner transfers, missing history and a second-week baseline. Inspect each audience's report for forbidden owners before testing delivery.

Once accepted, create the **production** database/volume, set `dry_run:false`, restart the service, point n8n at that instance and run against private test Slack/mailboxes. A dry-run record should not be toggled into live mode in the same database: its frozen config deliberately prevents that. Keep preview and production separated. After successful live tests and real recipient mapping, publish the weekly and recovery workflows. No deployment or messages were performed as part of this delivered package.

## 6. Optional reporting-dashboard graphic

Supply an actual export/download URL that your BI platform or internal exporter supports. This is an input contract, not a claimed HubSpot dashboard endpoint:

```json
{
  "audience_id": "manager",
  "url": "https://YOUR-APPROVED-EXPORT-HOST/path/to/scoped-report.png",
  "allowed_hosts": ["YOUR-APPROVED-EXPORT-HOST"],
  "token_env": "DASHBOARD_EXPORT_TOKEN"
}
```

Put entries in `dashboard_exports`. The operator must verify the export's scope, period and permitted viewers. The service cannot infer authorization from image pixels. Each image is attached only to its named audience, labeled supplemental, and never used in calculations. It must be an HTTPS PNG ≤8 MB without a redirect; POST/job/polling export flows require a provider-specific adapter. Optional export failure leaves the core digest intact with a visible note.

## 7. Recovery and reconciliation

Normal recovery loads the exact prepared report and sends only unresolved known-failure components. Five claim attempts per component is the limit. Completed components are not resent. Reports older than `delivery_max_age_hours` are blocked to avoid sending stale alerts days later; review and intentionally re-plan instead.

When a send is `unknown`, stop the service and inspect provider logs. Reconcile **only with evidence** that the provider accepted or rejected it. The offline utility records the evidence in the audit table. Example for a Slack parent proven delivered:

```sh
python -m service.manage --db /path/to/digest.sqlite3 reconcile \
  'acme:2026-10-05' 'slack:manager:CREALCHANNEL' root \
  --state sent --evidence 'Slack permalink / administrator verification' \
  --response-json '{"ts":"ACTUAL_PARENT_TIMESTAMP"}'
```

If proven not accepted, use `--state failed` so the next recovery retries. Never set failed merely because a timeout occurred. If the five-attempt budget is exhausted, investigate the cause and perform a reviewed ledger change before allowing further retries. Do not delete sent rows. For an expired upload allocation, confirm file state and reset only the relevant allocation/bytes/completion sequence with an audited database procedure.

For SMTP ambiguity, search the relay logs for the deterministic Message-ID (hash of run and destination); after confirmed acceptance, reconcile as sent. The mail-thread table normally updates at SMTP acceptance; a crash before it does may require restoring root/last Message-ID as part of manual reconciliation. A durable ID aids investigation; it is not a universal SMTP idempotency key.

## 8. Privacy, retention and operations

- Dedicated service/volume/credential set per workspace; do not reuse a global unscoped API token across tenants.
- Protect the raw snapshot and rendered artifact database with encrypted storage and least-privilege filesystem access. It contains deal names, owner IDs and private pipeline amounts.
- Workflow settings disable successful, failed and manual execution payload retention. The service retains minimal audit categories; errors do not include provider bodies/tokens in Slack. Enable restricted diagnostic retention only for a specific investigation and turn it off afterward.
- Service secrets are never returned to n8n; do not dump environment variables or `.env` into support logs. Secret rotation does not change business config if environment variable names stay constant.
- Review a retention policy (example: 180 days), preserve at least two baselines and export only to approved private locations. Preview retention pruning first: `python -m service.manage --db DB prune --days 180`. With service stopped and backup verified, `--apply` removes eligible completed runs/artifacts/deliveries/audit. Thread identifiers remain for continuity; delete those separately when decommissioning a recipient/workspace. Unresolved runs are not silently pruned.
- Configure an independent uptime/dead-man monitor for missed scheduled runs and a failed reporting service. The included admin route cannot notify through a service that is offline or a revoked Slack token.
- Monitor collection duration, page count/cap, stage failures, fallback frequency, incomplete destinations and unknown sends. The reference runs only one process, and long calls serialize subsequent requests.

CRM backfills are deliberately unsupported: current-state reads cannot reliably recreate a historical week. A warehouse with point-in-time records is required for that extension.
