Problem
We have zero visibility into how Archon is actually used in the wild. We don't know which bundled workflows get real usage, whether users are writing their own, or which user-facing features to invest in next. A lightweight, anonymous usage signal would unblock those product decisions without compromising the single-developer-tool ethos.
- What problem: No data on workflow adoption — we can't tell whether
archon-assist, archon-implement, archon-plan etc. are equally used, or whether any are dead weight. We also have no signal on how many users write their own workflows and what they call them.
- Who experiences it: Maintainers (product decisions), and by extension all users (better prioritization, less cruft).
- How often: Continuously — every roadmap decision today is made blind.
Proposed Solution
Integrate PostHog (posthog-node) for anonymous, opt-out-able telemetry. Emit one event — workflow_invoked — every time executeWorkflow() kicks off, regardless of trigger source (CLI, web, chat adapters, GitHub @mentions).
Event properties:
| Property |
Value |
Source |
workflow_name |
e.g. archon-implement, my-custom-flow |
workflow.name |
workflow_description |
First ~200 chars of the workflow description |
workflow.description |
workflow_source |
bundled | project | global |
Threaded from discovery |
platform |
cli | web | slack | telegram | discord | github | gitlab | gitea |
platform.getPlatformType() |
archon_version |
Semver from root package.json |
Build-time or runtime read |
$process_person_profile |
false |
Keeps events in the anonymous tier (no PostHog person profile created) |
Distinct ID: A stable anonymous UUID generated once on first run and persisted to ~/.archon/telemetry-id (or equivalent in ARCHON_HOME). Lets us count distinct installs and correlate workflow usage per-install, without any user identity. The ID never touches posthog.identify() so events stay in PostHog's cheaper anonymous tier.
No PII. Ever. No git remotes, no file paths, no usernames, no prompts/messages, no tokens. Workflow names and descriptions authored by the user are the only free-form strings, and description is truncated.
Research: Implementation Blueprint
Integration chokepoint
All invocation paths converge on a single function — executeWorkflow() in packages/workflows/src/executor.ts:230. There are three direct call sites:
- CLI:
packages/cli/src/commands/workflow.ts:613
- Orchestrator foreground/interactive/resume:
packages/core/src/orchestrator/orchestrator-agent.ts:276, 288, 314
- Orchestrator background (web async):
packages/core/src/orchestrator/orchestrator.ts:365
At executor.ts:618-623 the engine already fires a typed workflow_started event on the singleton WorkflowEventEmitter (packages/workflows/src/event-emitter.ts:27-32). That's the right hook site — subscribe once at server/CLI startup and call posthog.capture() inside the listener. Existing subscribers (the web SSE adapter) prove the pattern scales and stays error-isolated.
Data already in scope at the hook
| Field |
In scope? |
Notes |
workflow.name |
Yes |
Required field |
workflow.description |
Yes |
Required field, z.string().min(1) |
workflowRun.id |
Yes |
Generated by engine |
platform.getPlatformType() |
Yes |
Existing method on IWorkflowPlatform |
workflow.source (bundled/project) |
No |
WorkflowSource is carried by WorkflowWithSource during discovery (packages/workflows/src/schemas/workflow.ts:96-102) but stripped at all three call sites — they pass only WorkflowDefinition |
Implementation note: thread source: WorkflowSource through as an optional parameter to executeWorkflow() and extend WorkflowStartedEvent with an optional source field. All three call sites already have the WorkflowWithSource in scope (they do .map(ws => ws.workflow) to drop it); pass ws.source through instead.
SDK choice
posthog-node (server-side; v5.29.2). posthog-js is the browser SDK and does not belong in server code.
- Bun-compatible — PostHog docs explicitly list
bun add posthog-node as a supported install path.
Anonymous tracking pattern
PostHog requires a distinctId. For tool-analytics with no user identity:
- Generate a UUID on first run, persist to
~/.archon/telemetry-id.
- Send
$process_person_profile: false on every event — this puts events in PostHog's anonymous-tier table (no person joins, up to 4x cheaper, and no person profile is ever created).
- Never call
posthog.identify() on that ID.
Opt-out
Support three mechanisms, in priority order:
- Env var:
ARCHON_TELEMETRY_DISABLED=1 (Archon-specific)
- Env var:
DO_NOT_TRACK=1 (de facto standard honored by Astro/Bun/Prisma/Nuxt/Expo)
- Config:
telemetry.enabled: false in ~/.archon/config.yaml or repo .archon/config.yaml
Implementation: if any opt-out is set, call await posthog.disable() at init. This makes all subsequent capture() calls silent no-ops — no conditional guards needed throughout the codebase.
Lifecycle
- Server mode: hook
posthog.shutdown() on SIGTERM / SIGINT.
- CLI mode: each CLI command calls
await posthog.shutdown() at the end, or uses captureImmediate() to guarantee flush before process exit. Short-lived processes lose buffered events if they exit without flushing.
- Attach
posthog.on('error', ...) → log-and-swallow. Telemetry must never crash the app.
PostHog API key handling
phc_* is a public write-only key — safe to embed in source / distributed binaries. The OSS pattern is: maintainer bakes their own phc_* into a constant (ideally read from process.env.POSTHOG_API_KEY at build time, falling back to the embedded default). If the env var is unset and no key is baked in, telemetry self-disables silently.
Suggested module shape
New thin module (no new workspace package needed):
packages/core/src/services/telemetry.ts — singleton client, captureWorkflowInvoked() helper, opt-out handling, shutdown() export.
- Subscribe to
WorkflowEventEmitter once in packages/server/src/index.ts (server) and once in packages/cli/src/cli.ts (CLI).
posthog-node added to packages/core/package.json.
User Flow
Before (current)
Maintainer: "Which bundled workflow gets used the most?"
[!] No idea. Best we can do is count GitHub stars.
After (proposed)
[+] Every workflow invocation -> posthog.capture({ event: 'workflow_invoked', ... })
[+] PostHog UI: Trends chart filtered by workflow_name, grouped by workflow_source
-> "archon-implement: 1,240 runs / 60% bundled / 40% project-defined this week"
[+] Users who opt out (ARCHON_TELEMETRY_DISABLED=1 or DO_NOT_TRACK=1): silent no-op
Alternatives Considered
| Alternative |
Pros |
Cons |
Why not chosen |
| Self-hosted Plausible / Umami |
Privacy-first, full control |
Extra infra to run; weaker event-analytics UX |
Overkill for single-developer tool; PostHog has a generous free tier |
| Segment + multiple destinations |
Flexibility |
Another dependency layer; cost |
YAGNI — we have one destination |
| Custom endpoint (post to archon-api) |
No third-party |
We'd be building analytics infra |
Distracts from product work |
Instrument every lifecycle event (workflow_completed, node_started, ...) |
More data |
Noise, cost, analysis paralysis |
Start with one event; expand only if a question demands it |
Store source at capture time via re-discovery |
Less threading |
Couples telemetry to the discovery layer |
Threading through executeWorkflow is trivial and keeps boundaries clean |
Scope
- Package(s) likely affected:
core (telemetry module + config), workflows (thread source param + extend event type), server (startup subscribe + SIGTERM flush), cli (startup subscribe + shutdown-on-exit), paths (optional — anonymous ID helper could live next to archon-paths.ts)
- Breaking change? No
- Database changes needed? No
- New external dependencies? Yes —
posthog-node (one runtime dep)
Security Considerations
- New permissions/capabilities? No — no new permissions needed on the user's machine.
- New external network calls? Yes — outbound HTTPS to
us.i.posthog.com (or EU). Batched, async, non-blocking. Fully opt-out-able.
- Secrets/tokens handling? No — the
phc_* key is public-write-only; safe to embed.
- PII audit: confirm at code review that nothing downstream adds PII. Only
workflow_name + workflow_description are free-form strings, both authored by the user.
- Anonymous-tier guarantee: the chosen
distinctId must never be passed to posthog.identify() — add a lint/comment guard.
- Doc update: add a "Telemetry" section to the README explaining what is collected, how to opt out, and linking to this issue.
Definition of Done
References
Problem
We have zero visibility into how Archon is actually used in the wild. We don't know which bundled workflows get real usage, whether users are writing their own, or which user-facing features to invest in next. A lightweight, anonymous usage signal would unblock those product decisions without compromising the single-developer-tool ethos.
archon-assist,archon-implement,archon-planetc. are equally used, or whether any are dead weight. We also have no signal on how many users write their own workflows and what they call them.Proposed Solution
Integrate PostHog (
posthog-node) for anonymous, opt-out-able telemetry. Emit one event —workflow_invoked— every timeexecuteWorkflow()kicks off, regardless of trigger source (CLI, web, chat adapters, GitHub @mentions).Event properties:
workflow_namearchon-implement,my-custom-flowworkflow.nameworkflow_descriptionworkflow.descriptionworkflow_sourcebundled|project|globalplatformcli|web|slack|telegram|discord|github|gitlab|giteaplatform.getPlatformType()archon_versionpackage.json$process_person_profilefalseDistinct ID: A stable anonymous UUID generated once on first run and persisted to
~/.archon/telemetry-id(or equivalent inARCHON_HOME). Lets us count distinct installs and correlate workflow usage per-install, without any user identity. The ID never touchesposthog.identify()so events stay in PostHog's cheaper anonymous tier.No PII. Ever. No git remotes, no file paths, no usernames, no prompts/messages, no tokens. Workflow names and descriptions authored by the user are the only free-form strings, and description is truncated.
Research: Implementation Blueprint
Integration chokepoint
All invocation paths converge on a single function —
executeWorkflow()inpackages/workflows/src/executor.ts:230. There are three direct call sites:packages/cli/src/commands/workflow.ts:613packages/core/src/orchestrator/orchestrator-agent.ts:276,288,314packages/core/src/orchestrator/orchestrator.ts:365At
executor.ts:618-623the engine already fires a typedworkflow_startedevent on the singletonWorkflowEventEmitter(packages/workflows/src/event-emitter.ts:27-32). That's the right hook site — subscribe once at server/CLI startup and callposthog.capture()inside the listener. Existing subscribers (the web SSE adapter) prove the pattern scales and stays error-isolated.Data already in scope at the hook
workflow.nameworkflow.descriptionz.string().min(1)workflowRun.idplatform.getPlatformType()IWorkflowPlatformworkflow.source(bundled/project)WorkflowSourceis carried byWorkflowWithSourceduring discovery (packages/workflows/src/schemas/workflow.ts:96-102) but stripped at all three call sites — they pass onlyWorkflowDefinitionImplementation note: thread
source: WorkflowSourcethrough as an optional parameter toexecuteWorkflow()and extendWorkflowStartedEventwith an optionalsourcefield. All three call sites already have theWorkflowWithSourcein scope (they do.map(ws => ws.workflow)to drop it); passws.sourcethrough instead.SDK choice
posthog-node(server-side; v5.29.2).posthog-jsis the browser SDK and does not belong in server code.bun add posthog-nodeas a supported install path.Anonymous tracking pattern
PostHog requires a
distinctId. For tool-analytics with no user identity:~/.archon/telemetry-id.$process_person_profile: falseon every event — this puts events in PostHog's anonymous-tier table (no person joins, up to 4x cheaper, and no person profile is ever created).posthog.identify()on that ID.Opt-out
Support three mechanisms, in priority order:
ARCHON_TELEMETRY_DISABLED=1(Archon-specific)DO_NOT_TRACK=1(de facto standard honored by Astro/Bun/Prisma/Nuxt/Expo)telemetry.enabled: falsein~/.archon/config.yamlor repo.archon/config.yamlImplementation: if any opt-out is set, call
await posthog.disable()at init. This makes all subsequentcapture()calls silent no-ops — no conditional guards needed throughout the codebase.Lifecycle
posthog.shutdown()onSIGTERM/SIGINT.await posthog.shutdown()at the end, or usescaptureImmediate()to guarantee flush before process exit. Short-lived processes lose buffered events if they exit without flushing.posthog.on('error', ...)→ log-and-swallow. Telemetry must never crash the app.PostHog API key handling
phc_*is a public write-only key — safe to embed in source / distributed binaries. The OSS pattern is: maintainer bakes their ownphc_*into a constant (ideally read fromprocess.env.POSTHOG_API_KEYat build time, falling back to the embedded default). If the env var is unset and no key is baked in, telemetry self-disables silently.Suggested module shape
New thin module (no new workspace package needed):
packages/core/src/services/telemetry.ts— singleton client,captureWorkflowInvoked()helper, opt-out handling,shutdown()export.WorkflowEventEmitteronce inpackages/server/src/index.ts(server) and once inpackages/cli/src/cli.ts(CLI).posthog-nodeadded topackages/core/package.json.User Flow
Before (current)
After (proposed)
Alternatives Considered
workflow_completed,node_started, ...)sourceat capture time via re-discoveryexecuteWorkflowis trivial and keeps boundaries cleanScope
core(telemetry module + config),workflows(threadsourceparam + extend event type),server(startup subscribe + SIGTERM flush),cli(startup subscribe + shutdown-on-exit),paths(optional — anonymous ID helper could live next toarchon-paths.ts)posthog-node(one runtime dep)Security Considerations
us.i.posthog.com(or EU). Batched, async, non-blocking. Fully opt-out-able.phc_*key is public-write-only; safe to embed.workflow_name+workflow_descriptionare free-form strings, both authored by the user.distinctIdmust never be passed toposthog.identify()— add a lint/comment guard.Definition of Done
posthog-nodeadded as a dependency; telemetry module initializes a singleton client on startup (server + CLI)workflow_invokedevent fires from theWorkflowEventEmitter.workflow_startedsubscription with all six properties listed aboveWorkflowStartedEventextended with optionalsource;executeWorkflow()threadssourcefrom all three call sites~/.archon/telemetry-idon first run; stable across restartsARCHON_TELEMETRY_DISABLED=1,DO_NOT_TRACK=1, ortelemetry.enabled: falsein configposthog.shutdown()hooked onSIGTERM/SIGINTfor server and called at end of each CLI commandposthog.on('error', ...)handler logs-and-swallows; no telemetry failure can crash Archoncapture()is called with the right properties; unit test for opt-out env varsReferences