@hogsend/cli 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +2238 -75
- package/dist/bin.js.map +1 -1
- package/package.json +9 -1
- package/skills/hogsend-authoring-buckets/SKILL.md +69 -0
- package/skills/hogsend-authoring-buckets/references/bucket-id-aliases.md +117 -0
- package/skills/hogsend-authoring-buckets/references/bucket-meta.md +142 -0
- package/skills/hogsend-authoring-buckets/references/buckets-vs-journeys.md +96 -0
- package/skills/hogsend-authoring-buckets/references/register-a-bucket.md +129 -0
- package/skills/hogsend-authoring-emails/SKILL.md +68 -0
- package/skills/hogsend-authoring-emails/references/email-components.md +112 -0
- package/skills/hogsend-authoring-emails/references/preview-and-render.md +116 -0
- package/skills/hogsend-authoring-emails/references/template-four-file-contract.md +134 -0
- package/skills/hogsend-authoring-emails/references/tracking-and-unsubscribe.md +127 -0
- package/skills/hogsend-authoring-journeys/SKILL.md +88 -0
- package/skills/hogsend-authoring-journeys/references/branch-on-engagement.md +93 -0
- package/skills/hogsend-authoring-journeys/references/journey-context.md +110 -0
- package/skills/hogsend-authoring-journeys/references/journey-meta.md +142 -0
- package/skills/hogsend-authoring-journeys/references/register-a-journey.md +99 -0
- package/skills/hogsend-authoring-journeys/references/sending-email-from-a-journey.md +82 -0
- package/skills/hogsend-cli/SKILL.md +81 -0
- package/skills/hogsend-cli/references/debug-a-journey.md +66 -0
- package/skills/hogsend-cli/references/manage-journeys.md +53 -0
- package/skills/hogsend-cli/references/query-stats.md +66 -0
- package/skills/hogsend-cli/references/setup-local.md +52 -0
- package/skills/hogsend-conditions/SKILL.md +70 -0
- package/skills/hogsend-conditions/references/condition-types.md +251 -0
- package/skills/hogsend-conditions/references/durations.md +90 -0
- package/skills/hogsend-conditions/references/examples.md +188 -0
- package/skills/hogsend-database/SKILL.md +70 -0
- package/skills/hogsend-database/references/client-track-schema.md +97 -0
- package/skills/hogsend-database/references/migrations.md +132 -0
- package/skills/hogsend-database/references/schema-drift.md +123 -0
- package/skills/hogsend-deploy/SKILL.md +62 -0
- package/skills/hogsend-deploy/references/env-and-secrets.md +118 -0
- package/skills/hogsend-deploy/references/railway-two-services.md +122 -0
- package/skills/hogsend-deploy/references/upgrade-engine.md +92 -0
- package/skills/hogsend-webhooks-and-workflows/SKILL.md +68 -0
- package/skills/hogsend-webhooks-and-workflows/references/backfill-pattern.md +148 -0
- package/skills/hogsend-webhooks-and-workflows/references/custom-workflow.md +156 -0
- package/skills/hogsend-webhooks-and-workflows/references/webhook-source.md +172 -0
- package/src/bin.ts +73 -111
- package/src/commands/contacts.ts +316 -0
- package/src/commands/doctor.ts +239 -0
- package/src/commands/eject.ts +106 -0
- package/src/commands/events.ts +154 -0
- package/src/commands/index.ts +36 -0
- package/src/commands/journeys.ts +343 -0
- package/src/commands/patch.ts +80 -0
- package/src/commands/setup.ts +322 -0
- package/src/commands/skills.ts +208 -0
- package/src/commands/stats.ts +87 -0
- package/src/commands/studio.ts +261 -0
- package/src/commands/types.ts +41 -0
- package/src/commands/upgrade.ts +245 -0
- package/src/index.ts +2 -0
- package/src/lib/config.ts +147 -0
- package/src/lib/http.ts +145 -0
- package/src/lib/output.ts +185 -0
- package/src/lib/prompt.ts +17 -0
- package/src/lib/skills.ts +186 -0
- package/studio/assets/index-BVA9GZqq.css +1 -0
- package/studio/assets/index-kPwzOOyG.js +230 -0
- package/studio/index.html +13 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# JourneyMeta — trigger, entryLimit, exitOn, suppress
|
|
2
|
+
|
|
3
|
+
`meta` is the static declaration of who enters a journey, how often, and what
|
|
4
|
+
pulls them out. It is the `JourneyMeta` type from `@hogsend/core`:
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
interface JourneyMeta {
|
|
8
|
+
id: string; // stable unique id — used for state + ENABLED_JOURNEYS
|
|
9
|
+
name: string; // human label (becomes user.journeyName)
|
|
10
|
+
description?: string;
|
|
11
|
+
enabled: boolean; // master on/off for this journey
|
|
12
|
+
|
|
13
|
+
trigger: {
|
|
14
|
+
event: string; // the event name that enrolls a user
|
|
15
|
+
where?: PropertyCondition[]; // optional gate on event properties
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
entryLimit: "once" | "once_per_period" | "unlimited";
|
|
19
|
+
entryPeriod?: DurationObject; // required-in-practice for once_per_period
|
|
20
|
+
|
|
21
|
+
exitOn?: Array<{
|
|
22
|
+
event: string;
|
|
23
|
+
where?: PropertyCondition[];
|
|
24
|
+
}>;
|
|
25
|
+
|
|
26
|
+
suppress: DurationObject; // declared cool-down (required field; see note)
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Fields in detail
|
|
31
|
+
|
|
32
|
+
### `trigger.event`
|
|
33
|
+
|
|
34
|
+
The event name that enrolls a user. Each journey declares `onEvents:
|
|
35
|
+
[trigger.event]` on its Hatchet durable task, so when `POST /v1/ingest` (or a
|
|
36
|
+
webhook source) pushes that event, Hatchet routes it straight to this journey.
|
|
37
|
+
Use a constant from your `src/journeys/constants/`:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
trigger: { event: Events.USER_CREATED },
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### `trigger.where` (optional)
|
|
44
|
+
|
|
45
|
+
A `PropertyCondition[]` evaluated against the **enrolling event's properties**.
|
|
46
|
+
If present and not met, the event is skipped with reason
|
|
47
|
+
`trigger_conditions_not_met`. Use it to enroll only a slice of an event:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
trigger: {
|
|
51
|
+
event: Events.USER_CREATED,
|
|
52
|
+
where: [{ property: "plan", operator: "equals", value: "pro" }],
|
|
53
|
+
},
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
For the full operator set and how `PropertyCondition` is shaped, see the
|
|
57
|
+
**hogsend-conditions** skill.
|
|
58
|
+
|
|
59
|
+
### `entryLimit` + `entryPeriod`
|
|
60
|
+
|
|
61
|
+
Controls re-entry:
|
|
62
|
+
|
|
63
|
+
- `"once"` — a user can ever enter this journey exactly one time. A second
|
|
64
|
+
matching event is skipped (`already_entered_once`). Checked against ALL prior
|
|
65
|
+
states for the user+journey, regardless of completion.
|
|
66
|
+
- `"once_per_period"` — re-entry allowed only after `entryPeriod` has elapsed
|
|
67
|
+
since the user's most recent enrollment. Defaults to `hours(24)` if
|
|
68
|
+
`entryPeriod` is omitted, so set it explicitly:
|
|
69
|
+
```ts
|
|
70
|
+
entryLimit: "once_per_period",
|
|
71
|
+
entryPeriod: days(7),
|
|
72
|
+
```
|
|
73
|
+
- `"unlimited"` — every matching event enrolls (subject to the active-state
|
|
74
|
+
guard below). Good for test/smoke journeys.
|
|
75
|
+
|
|
76
|
+
### `exitOn` (optional)
|
|
77
|
+
|
|
78
|
+
Events that pull a user OUT of any in-flight run of this journey. Evaluated by
|
|
79
|
+
the ingestion pipeline whenever a new event arrives for a user with an active
|
|
80
|
+
journey state — if the incoming event name matches an `exitOn` rule (and its
|
|
81
|
+
optional `where` passes), the active run is exited:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
exitOn: [
|
|
85
|
+
{ event: Events.USER_DELETED },
|
|
86
|
+
{ event: "subscription.cancelled", where: [{ property: "reason", operator: "equals", value: "churn" }] },
|
|
87
|
+
],
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### `suppress`
|
|
91
|
+
|
|
92
|
+
A **required** `DurationObject` field declaring an intended cool-down before
|
|
93
|
+
re-entry. Note: the engine's enrollment gates do NOT currently read `suppress` —
|
|
94
|
+
actual re-entry timing is enforced by `entryLimit` + `entryPeriod` (above). It is
|
|
95
|
+
stored as journey metadata and surfaced on the admin journeys API. Treat it as
|
|
96
|
+
the declarative cool-down you pair with `entryLimit` (e.g.
|
|
97
|
+
`entryLimit: "once_per_period"`, `entryPeriod: days(7)`, `suppress: hours(12)`);
|
|
98
|
+
use `hours(0)` on `"unlimited"` test journeys. Because it is required, always set
|
|
99
|
+
it — `hours(0)` when you mean "none".
|
|
100
|
+
|
|
101
|
+
### `enabled`
|
|
102
|
+
|
|
103
|
+
Master switch. `enabled: false` makes every enrollment skip with reason
|
|
104
|
+
`journey_disabled`. (Admins can ALSO disable a journey at runtime via a
|
|
105
|
+
`journeyConfigs` override — that yields `journey_disabled_by_admin`. Toggle that
|
|
106
|
+
at runtime with the **hogsend-cli** skill's `journeys enable/disable`.)
|
|
107
|
+
|
|
108
|
+
## The 4-gate enrollment order
|
|
109
|
+
|
|
110
|
+
When the trigger event arrives, the journey task runs these gates IN ORDER
|
|
111
|
+
before `run()` executes. Any failing gate returns `{ status: "skipped", reason }`
|
|
112
|
+
and creates NO state:
|
|
113
|
+
|
|
114
|
+
1. **`meta.enabled`** (and the admin `journeyConfigs` override) →
|
|
115
|
+
`journey_disabled` / `journey_disabled_by_admin`.
|
|
116
|
+
2. **`trigger.where`** against the event properties → `trigger_conditions_not_met`.
|
|
117
|
+
3. **`entryLimit`** (`checkEntryLimit`) → `already_entered_once` /
|
|
118
|
+
`period_not_elapsed`.
|
|
119
|
+
4. **Email preferences** (`checkEmailPreferences`) — if the user is unsubscribed
|
|
120
|
+
from all → `user_unsubscribed`.
|
|
121
|
+
|
|
122
|
+
After the gates, an **active-state guard** prevents concurrent enrollment: if a
|
|
123
|
+
state in status `active` or `waiting` already exists for this user+journey, the
|
|
124
|
+
event is skipped (`already_active`). This is why a single user never has two
|
|
125
|
+
overlapping runs of the same journey.
|
|
126
|
+
|
|
127
|
+
## State transitions
|
|
128
|
+
|
|
129
|
+
A `journeyStates` row tracks each run. Once the gates pass:
|
|
130
|
+
|
|
131
|
+
- **enter** → row created with `status: "active"`, `currentNodeId: "start"`.
|
|
132
|
+
- **`ctx.sleep` / `ctx.sleepUntil`** → `status: "waiting"` for the duration, back
|
|
133
|
+
to `"active"` on resume.
|
|
134
|
+
- **`run()` returns** → `status: "completed"`, `completedAt` set, and a
|
|
135
|
+
`journey:completed` event is pushed.
|
|
136
|
+
- **`run()` throws** → `status: "failed"`, `errorMessage` recorded, and a
|
|
137
|
+
`journey:failed` event is pushed; the error re-throws so Hatchet sees the
|
|
138
|
+
failure.
|
|
139
|
+
|
|
140
|
+
Because the gates run before any state is created, a skipped event is invisible
|
|
141
|
+
in `journeyStates` — to debug "why didn't this user enroll?", check the gate
|
|
142
|
+
order above and inspect events with the **hogsend-cli** skill.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Registering a journey
|
|
2
|
+
|
|
3
|
+
Defining a journey is not enough — it has to be in the `journeys` array that the
|
|
4
|
+
client and worker receive. Both processes (HTTP API + Hatchet worker) must see
|
|
5
|
+
the same array: the API needs it to route ingested events, the worker needs it to
|
|
6
|
+
register the durable task that actually runs `run()`.
|
|
7
|
+
|
|
8
|
+
## 1. Export from `src/journeys/index.ts`
|
|
9
|
+
|
|
10
|
+
Add your journey to the `journeys` array (and re-export it for tests / direct
|
|
11
|
+
reference):
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import type { DefinedJourney } from "@hogsend/engine";
|
|
15
|
+
import { activation } from "./activation.js";
|
|
16
|
+
import { testOnboarding } from "./test-onboarding.js";
|
|
17
|
+
import { welcome } from "./welcome.js";
|
|
18
|
+
|
|
19
|
+
export const journeys: DefinedJourney[] = [
|
|
20
|
+
welcome,
|
|
21
|
+
testOnboarding,
|
|
22
|
+
activation, // <-- your new journey
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
export { activation, testOnboarding, welcome };
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The exported `journeys` array is the single source of truth that both entry
|
|
29
|
+
points consume.
|
|
30
|
+
|
|
31
|
+
## 2. It is already threaded into the client + worker
|
|
32
|
+
|
|
33
|
+
In a scaffolded app both entry points import that same array — you usually do
|
|
34
|
+
NOT need to touch these files, just confirm they pass `journeys`:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
// src/index.ts (HTTP API)
|
|
38
|
+
import { createApp, createHogsendClient } from "@hogsend/engine";
|
|
39
|
+
import { buckets } from "./buckets/index.js";
|
|
40
|
+
import { templates } from "./emails/index.js";
|
|
41
|
+
import { journeys } from "./journeys/index.js";
|
|
42
|
+
import { webhookSources } from "./webhook-sources/index.js";
|
|
43
|
+
|
|
44
|
+
const client = createHogsendClient({ journeys, buckets, email: { templates } });
|
|
45
|
+
const app = createApp(client, { webhookSources });
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
// src/worker.ts (Hatchet worker — this is what runs run())
|
|
50
|
+
import { createHogsendClient, createWorker } from "@hogsend/engine";
|
|
51
|
+
import { buckets } from "./buckets/index.js";
|
|
52
|
+
import { templates } from "./emails/index.js";
|
|
53
|
+
import { journeys } from "./journeys/index.js";
|
|
54
|
+
import { extraWorkflows } from "./workflows/index.js";
|
|
55
|
+
|
|
56
|
+
const client = createHogsendClient({ journeys, buckets, email: { templates } });
|
|
57
|
+
const worker = createWorker({ container: client, journeys, buckets, extraWorkflows });
|
|
58
|
+
await worker.start();
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
If you build a new entry point or a test harness, the rule is: **pass the same
|
|
62
|
+
`journeys` array to both `createHogsendClient({ journeys })` and
|
|
63
|
+
`createWorker({ container, journeys })`.** A journey missing from the worker is
|
|
64
|
+
defined but never executes; missing from the client and ingested events won't
|
|
65
|
+
route to it. (`buckets` and `extraWorkflows` are the sibling content arrays the
|
|
66
|
+
scaffold threads through the same way — pass them when present.)
|
|
67
|
+
|
|
68
|
+
## 3. (Optional) `ENABLED_JOURNEYS`
|
|
69
|
+
|
|
70
|
+
The `ENABLED_JOURNEYS` env var filters which registered journeys actually load —
|
|
71
|
+
comma-separated ids, or `*` (or empty/unset) for all:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
# only these two run; everything else is registered but inert
|
|
75
|
+
ENABLED_JOURNEYS=welcome,activation
|
|
76
|
+
|
|
77
|
+
# all journeys (default)
|
|
78
|
+
ENABLED_JOURNEYS=*
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The filter matches on `meta.id`, so keep ids stable. It is applied identically
|
|
82
|
+
when building the registry (client) and when selecting durable tasks (worker), so
|
|
83
|
+
a journey is either fully on or fully off across both processes. Use it to ship a
|
|
84
|
+
journey's code but keep it dark until you flip the env var — distinct from
|
|
85
|
+
`meta.enabled: false` (code-level off) and the runtime admin toggle (see the
|
|
86
|
+
**hogsend-cli** skill's `journeys enable/disable`).
|
|
87
|
+
|
|
88
|
+
## Checklist for a new journey
|
|
89
|
+
|
|
90
|
+
1. New file in `src/journeys/` using `defineJourney({ meta, run })`.
|
|
91
|
+
2. Any new event / template keys added to `src/journeys/constants/`.
|
|
92
|
+
3. New email? component + registry entry under `src/emails/` for each
|
|
93
|
+
`Templates.*` key you send.
|
|
94
|
+
4. Journey added to the `journeys` array in `src/journeys/index.ts`.
|
|
95
|
+
5. Confirm `src/index.ts` and `src/worker.ts` both receive that array.
|
|
96
|
+
6. If gating by env, add the id to `ENABLED_JOURNEYS`.
|
|
97
|
+
|
|
98
|
+
To smoke-test it end-to-end against a running instance (enroll a user, watch the
|
|
99
|
+
state reach `completed`), use the **hogsend-cli** skill.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Sending email from a journey
|
|
2
|
+
|
|
3
|
+
`sendEmail()` is a **standalone import from `@hogsend/engine`** — it is NOT a
|
|
4
|
+
method on `ctx`. It renders the template, checks the user's email preferences,
|
|
5
|
+
rewrites links + injects the open pixel (tracking), writes the `email_sends` row,
|
|
6
|
+
and hands off to the provider. You just call it with a template key and props.
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { defineJourney, sendEmail } from "@hogsend/engine";
|
|
10
|
+
import { Events, Templates } from "./constants/index.js";
|
|
11
|
+
|
|
12
|
+
export const welcome = defineJourney({
|
|
13
|
+
meta: { /* ... */ },
|
|
14
|
+
run: async (user, ctx) => {
|
|
15
|
+
await sendEmail({
|
|
16
|
+
to: user.email, // recipient
|
|
17
|
+
userId: user.id, // external id (for prefs + tracking)
|
|
18
|
+
journeyStateId: user.stateId, // attributes the send to THIS run
|
|
19
|
+
template: Templates.ACTIVATION_WELCOME, // a key from your emails registry
|
|
20
|
+
subject: "Welcome — let's get you set up",
|
|
21
|
+
journeyName: user.journeyName, // shows on the send record / tags
|
|
22
|
+
props: { firstName: user.properties.firstName }, // template props (optional)
|
|
23
|
+
});
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## The options (`SendEmailOptions`)
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
interface SendEmailOptions {
|
|
32
|
+
to: string; // required — recipient email
|
|
33
|
+
userId: string; // required — external user id
|
|
34
|
+
template: string; // required — registry key (use Templates.*)
|
|
35
|
+
subject: string; // required
|
|
36
|
+
journeyName?: string; // attribution label (defaults to template)
|
|
37
|
+
journeyStateId?: string; // pass user.stateId to tie to this run
|
|
38
|
+
props?: Record<string, unknown>; // props handed to the template component
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`name` is auto-derived if you don't pass it: `props.firstName` → `props.name` →
|
|
43
|
+
the local-part of the email → `"there"`. So a template that renders `{name}`
|
|
44
|
+
always has something. Pass `firstName` in `props` for a real name.
|
|
45
|
+
|
|
46
|
+
## The result (`SendEmailResult`)
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
interface SendEmailResult {
|
|
50
|
+
emailSendId: string; // id of the email_sends row — keep it if you need to correlate
|
|
51
|
+
sentAt: string; // ISO timestamp
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const { emailSendId, sentAt } = await sendEmail({ /* ... */ });
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Template keys must exist in your registry
|
|
58
|
+
|
|
59
|
+
`template` is a `Templates.*` constant from your `src/journeys/constants/`. Each
|
|
60
|
+
key must resolve to a real template in `src/emails/` — the constant value (e.g.
|
|
61
|
+
`"activation/welcome"`) is the registry key. If you send a new email, first add:
|
|
62
|
+
|
|
63
|
+
1. the template component + registry entry under `src/emails/`,
|
|
64
|
+
2. the matching `Templates` key in `src/journeys/constants/`,
|
|
65
|
+
|
|
66
|
+
then reference it here as `Templates.YOUR_KEY`. A mismatched key fails at render
|
|
67
|
+
time, not compile time, so keep them in lockstep.
|
|
68
|
+
|
|
69
|
+
## Preferences, tracking, and idempotency
|
|
70
|
+
|
|
71
|
+
- `sendEmail` checks the user's email preferences internally; an unsubscribed
|
|
72
|
+
user is not emailed. You do not need to gate it — though after a long
|
|
73
|
+
`ctx.sleep` it is good practice to branch on `ctx.guard.isSubscribed()` first
|
|
74
|
+
(see `references/branch-on-engagement.md`).
|
|
75
|
+
- Link-click + open tracking is applied automatically by the engine mailer —
|
|
76
|
+
you get it regardless of which provider is configured.
|
|
77
|
+
- `sendEmail` is **not** itself deduped — calling it twice sends twice. Guard
|
|
78
|
+
repeat sends with `ctx.history.email({ email, template })` when a journey can
|
|
79
|
+
re-run a step.
|
|
80
|
+
|
|
81
|
+
For SMS / push / Slack, import the relevant standalone sender — those are also
|
|
82
|
+
plain function imports, never on `ctx`.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: hogsend-cli
|
|
3
|
+
description: Use when an agent needs to inspect or operate a running Hogsend lifecycle engine — querying metrics/contacts/events, listing or enabling/disabling journeys, checking health, or onboarding a local instance — by driving the consolidated `hogsend` CLI. Every data command supports --json for machine-readable output.
|
|
4
|
+
license: MIT
|
|
5
|
+
metadata:
|
|
6
|
+
author: withSeismic
|
|
7
|
+
version: "1.0.0"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Hogsend CLI
|
|
11
|
+
|
|
12
|
+
The `hogsend` CLI is the agent-native interface to a Hogsend app (the
|
|
13
|
+
code-first lifecycle orchestration engine on PostHog + Resend). It wraps the
|
|
14
|
+
app's `/v1/admin/*` and `/v1/health` HTTP routes, so it works against ANY
|
|
15
|
+
running instance — local (`http://localhost:3002`) or production — without
|
|
16
|
+
importing the database.
|
|
17
|
+
|
|
18
|
+
## The --json contract (READ THIS FIRST)
|
|
19
|
+
|
|
20
|
+
Every data/read command takes a global `--json` flag. In `--json` mode the
|
|
21
|
+
command prints EXACTLY ONE valid JSON document to stdout and nothing else — no
|
|
22
|
+
spinners, no color, no prose. **Always pass `--json` when you are parsing the
|
|
23
|
+
output programmatically.** Without it, you get a human-pretty table/keyvalue
|
|
24
|
+
rendering meant for a terminal.
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
hogsend stats --json
|
|
28
|
+
hogsend journeys list --json
|
|
29
|
+
hogsend contacts get user_123 --json
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
On error in `--json` mode the CLI prints `{"error":"<message>"}` to stdout and
|
|
33
|
+
exits 1. On success it exits 0 (doctor is the exception — see below).
|
|
34
|
+
|
|
35
|
+
## Connecting to an instance
|
|
36
|
+
|
|
37
|
+
Two global flags / env vars control which instance you talk to:
|
|
38
|
+
|
|
39
|
+
- Base URL: `--url <baseUrl>` > `HOGSEND_API_URL` env > `.env` `HOGSEND_API_URL`
|
|
40
|
+
> default `http://localhost:3002`.
|
|
41
|
+
- Admin key: `--admin-key <key>` > `HOGSEND_ADMIN_KEY` env > `ADMIN_API_KEY`
|
|
42
|
+
env > the `.env` equivalents. Sent as `Authorization: Bearer <key>`.
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
hogsend stats --url https://api.example.com --admin-key "$ADMIN_API_KEY" --json
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`doctor` hits the unauthenticated `/v1/health` and needs no admin key. Every
|
|
49
|
+
other data command requires one.
|
|
50
|
+
|
|
51
|
+
## Command map
|
|
52
|
+
|
|
53
|
+
| Command | Purpose |
|
|
54
|
+
|---------|---------|
|
|
55
|
+
| `hogsend doctor` | Health + schema-drift verdict (reachability check). |
|
|
56
|
+
| `hogsend stats` | Overview metrics (contacts, emails, bounce/unsub rates). |
|
|
57
|
+
| `hogsend journeys list/get/enable/disable` | Inspect + toggle journeys. |
|
|
58
|
+
| `hogsend contacts list/get/timeline` | Inspect contacts + their activity. |
|
|
59
|
+
| `hogsend events <userId>` | Raw event stream for one user. |
|
|
60
|
+
| `hogsend skills list/add` | Manage these bundled agent skills. |
|
|
61
|
+
| `hogsend upgrade` | Bump `@hogsend/*` deps to latest + refresh vendored skills. |
|
|
62
|
+
| `hogsend setup` | Interactive LOCAL onboarding (docker, secret, migrate). |
|
|
63
|
+
| `hogsend eject <pkg>` | Vendor a `@hogsend/*` package (unchanged). |
|
|
64
|
+
| `hogsend patch <pkg>` | Wrap `pnpm patch` (unchanged). |
|
|
65
|
+
|
|
66
|
+
Run `hogsend <command> --help` for per-command usage.
|
|
67
|
+
|
|
68
|
+
## Task playbooks (load the matching reference)
|
|
69
|
+
|
|
70
|
+
- **Query metrics / analyse data** → `references/query-stats.md`
|
|
71
|
+
- **List, inspect, enable or disable journeys** → `references/manage-journeys.md`
|
|
72
|
+
- **Debug why a user did / didn't enroll** → `references/debug-a-journey.md`
|
|
73
|
+
- **Set up a local instance** → `references/setup-local.md`
|
|
74
|
+
|
|
75
|
+
## Golden rules for agents
|
|
76
|
+
|
|
77
|
+
1. Pass `--json` whenever you will parse output. Never screen-scrape the table.
|
|
78
|
+
2. Start a debugging session with `hogsend doctor --json` to confirm the
|
|
79
|
+
instance is reachable and the schema is in sync before trusting other reads.
|
|
80
|
+
3. Enabling/disabling a journey is a write — confirm intent first.
|
|
81
|
+
4. Use `--limit`/`--offset` for pagination instead of dumping everything.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Debug a journey: why did (or didn't) a user enroll?
|
|
2
|
+
|
|
3
|
+
A repeatable trace using `doctor` + `journeys get` + `contacts timeline` +
|
|
4
|
+
`events`. Run every command with `--json` and read the output.
|
|
5
|
+
|
|
6
|
+
## Step 0 — confirm the instance is healthy
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
hogsend doctor --json
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Wraps `GET /v1/health`. Inspect the verdict:
|
|
13
|
+
|
|
14
|
+
- `ok` — healthy, proceed.
|
|
15
|
+
- `degraded` — a component (database / redis) is unhealthy; reads may be
|
|
16
|
+
unreliable. Exit code 1.
|
|
17
|
+
- `migration_pending` — the engine schema and the client schema are out of
|
|
18
|
+
sync (`schema.inSync = false`, `pending` migrations listed). Data may be
|
|
19
|
+
missing columns; fix the drift before trusting other queries.
|
|
20
|
+
- `unreachable` — could not connect (HTTP status 0). Check `--url` and that the
|
|
21
|
+
app is running. Exit code 1.
|
|
22
|
+
|
|
23
|
+
Do NOT trust downstream reads until doctor returns `ok`.
|
|
24
|
+
|
|
25
|
+
## Step 1 — understand the journey's entry rules
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
hogsend journeys get <journeyId> --json
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Read the `trigger` (which event enrolls a user) and any `trigger.where`
|
|
32
|
+
property conditions. Read `exitOn` rules (what removes a user). Enrollment is
|
|
33
|
+
gated, in order, by: `enabled` flag → trigger `where` conditions → entry limit
|
|
34
|
+
(once / once_per_period / unlimited) → email preferences (unsubscribed users
|
|
35
|
+
are skipped). Any failed gate means NO enrollment.
|
|
36
|
+
|
|
37
|
+
## Step 2 — look at the user's lifecycle
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
hogsend contacts get <userId> --json # subscribed? unsubscribed?
|
|
41
|
+
hogsend contacts timeline <userId> --json # merged events/emails/journeys
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The timeline shows whether the user already has an active/completed state for
|
|
45
|
+
this journey (entry limit may have blocked re-entry) and whether they're
|
|
46
|
+
unsubscribed (which skips email-sending journeys).
|
|
47
|
+
|
|
48
|
+
## Step 3 — verify the trigger event actually fired with the right props
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
hogsend events <userId> --event <triggerEvent> --json
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Wraps `GET /v1/admin/events?userId=<userId>`. Confirm the trigger event exists
|
|
55
|
+
for this user AND that its properties satisfy the journey's `trigger.where`
|
|
56
|
+
conditions. A missing event, or an event whose properties fail the `where`
|
|
57
|
+
check, is the most common reason a user did not enroll.
|
|
58
|
+
|
|
59
|
+
## Decision tree
|
|
60
|
+
|
|
61
|
+
- No trigger event in `events` → upstream isn't sending it (PostHog / webhook).
|
|
62
|
+
- Trigger event present but `where` mismatch → property condition not met.
|
|
63
|
+
- Already has a journey state → entry limit blocked re-enrollment.
|
|
64
|
+
- Contact unsubscribed → email journeys skipped at the preferences gate.
|
|
65
|
+
- Journey `enabled: false` (from `journeys get`) → no enrollment at all.
|
|
66
|
+
- `doctor` not `ok` → fix infra/schema first; the data is suspect.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Manage journeys
|
|
2
|
+
|
|
3
|
+
Inspect and toggle lifecycle journeys on a running Hogsend instance. `list` and
|
|
4
|
+
`get` are read-only; `enable`/`disable` are writes.
|
|
5
|
+
|
|
6
|
+
## List
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
hogsend journeys list --json
|
|
10
|
+
hogsend journeys list --enabled true --limit 50 --offset 0 --json
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Wraps `GET /v1/admin/journeys`. Filter with `--enabled <true|false>`, paginate
|
|
14
|
+
with `--limit`/`--offset`. Each row carries `id`, `name`, `enabled`, the
|
|
15
|
+
trigger event, and enrollment counts (active / completed / failed). Use the
|
|
16
|
+
`id` for every other journey command.
|
|
17
|
+
|
|
18
|
+
## Get detail
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
hogsend journeys get conversion-trial-upgrade --json
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Wraps `GET /v1/admin/journeys/{id}`. Returns the full definition view —
|
|
25
|
+
trigger (event + optional `where` conditions), `exitOn` rules, aggregate
|
|
26
|
+
counts, and a sample of recent `journeyStates` (so you can see who is currently
|
|
27
|
+
active / waiting / completed). This is your starting point before toggling a
|
|
28
|
+
journey.
|
|
29
|
+
|
|
30
|
+
## Enable / disable
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
hogsend journeys enable conversion-trial-upgrade --json
|
|
34
|
+
hogsend journeys disable conversion-trial-upgrade --json
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Wraps `PATCH /v1/admin/journeys/{id}` with `{ "enabled": true|false }`.
|
|
38
|
+
|
|
39
|
+
Safety notes:
|
|
40
|
+
|
|
41
|
+
- These are WRITES against a live system. Confirm the journey `id` with
|
|
42
|
+
`journeys get` first, and confirm intent with the human before disabling a
|
|
43
|
+
journey that has active enrollments.
|
|
44
|
+
- Disabling stops NEW enrollments. Contacts already mid-journey continue per
|
|
45
|
+
the engine's semantics — disabling is not a kill switch for in-flight states.
|
|
46
|
+
- After toggling, re-run `journeys get <id> --json` and confirm `enabled`
|
|
47
|
+
flipped as expected.
|
|
48
|
+
|
|
49
|
+
## Counts cheatsheet
|
|
50
|
+
|
|
51
|
+
When reading counts: `active` = currently enrolled (may be sleeping/waiting),
|
|
52
|
+
`completed` = finished the run, `failed` = errored. A journey with rising
|
|
53
|
+
`failed` counts is worth a `debug-a-journey` pass.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Query metrics, contacts, and events
|
|
2
|
+
|
|
3
|
+
Read-only analysis over a running Hogsend instance. Always pass `--json` when
|
|
4
|
+
parsing.
|
|
5
|
+
|
|
6
|
+
## Overview metrics
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
hogsend stats --json
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Wraps `GET /v1/admin/metrics/overview`. Returns (shape may vary slightly by
|
|
13
|
+
version):
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"totalContacts": 1234,
|
|
18
|
+
"activeJourneys": 5,
|
|
19
|
+
"emailsSent24h": 88,
|
|
20
|
+
"emailsSent7d": 540,
|
|
21
|
+
"emailsSent30d": 2100,
|
|
22
|
+
"bounceRate30d": 0.012,
|
|
23
|
+
"unsubscribeRate": 0.004
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Use this for a one-shot snapshot. `bounceRate30d` / `unsubscribeRate` are
|
|
28
|
+
fractions (multiply by 100 for a percentage). A rising bounce rate is the first
|
|
29
|
+
signal of a deliverability problem.
|
|
30
|
+
|
|
31
|
+
## Contacts
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# List with search + pagination
|
|
35
|
+
hogsend contacts list --search "@acme.com" --limit 50 --offset 0 --json
|
|
36
|
+
|
|
37
|
+
# A single contact by internal id OR externalId
|
|
38
|
+
hogsend contacts get user_123 --json
|
|
39
|
+
|
|
40
|
+
# Merged activity timeline (events + emails + journeys) for one contact
|
|
41
|
+
hogsend contacts timeline user_123 --json
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Wraps `GET /v1/admin/contacts`, `GET /v1/admin/contacts/{id}`, and
|
|
45
|
+
`GET /v1/admin/contacts/{id}/timeline`. `get` includes the contact record plus
|
|
46
|
+
email preferences (subscribed / unsubscribed). The timeline is the fastest way
|
|
47
|
+
to understand a single user's lifecycle history.
|
|
48
|
+
|
|
49
|
+
## Raw event stream
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
hogsend events user_123 --json
|
|
53
|
+
hogsend events user_123 --event "checkout.completed" --from 2026-01-01T00:00:00Z --to 2026-02-01T00:00:00Z --limit 100 --json
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Wraps `GET /v1/admin/events?userId=<userId>`. Filter by `--event`, time window
|
|
57
|
+
(`--from`/`--to`, ISO 8601), and paginate with `--limit`/`--offset`. Use this
|
|
58
|
+
when the timeline isn't granular enough and you need the exact event payloads
|
|
59
|
+
(e.g. to see which properties were present when a journey trigger fired).
|
|
60
|
+
|
|
61
|
+
## Analysis pattern
|
|
62
|
+
|
|
63
|
+
1. `hogsend stats --json` for the macro picture.
|
|
64
|
+
2. `hogsend contacts list --search ... --json` to find the cohort.
|
|
65
|
+
3. `hogsend contacts timeline <id> --json` to understand individual journeys.
|
|
66
|
+
4. `hogsend events <id> --event <name> --json` to inspect exact payloads.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Set up a local Hogsend instance
|
|
2
|
+
|
|
3
|
+
`hogsend setup` is interactive LOCAL onboarding — it mirrors the "next steps"
|
|
4
|
+
that `create-hogsend` prints after scaffolding. It is NOT a Railway / cloud
|
|
5
|
+
deploy flow.
|
|
6
|
+
|
|
7
|
+
## Run it
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
hogsend setup
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
What it does (interactively, with clack prompts + spinners):
|
|
14
|
+
|
|
15
|
+
1. `docker compose up -d` — starts TimescaleDB (Postgres), Redis, and
|
|
16
|
+
Hatchet-Lite locally.
|
|
17
|
+
2. Generates a `BETTER_AUTH_SECRET`.
|
|
18
|
+
3. Copies `.env.example` to `.env` if `.env` doesn't already exist (it won't
|
|
19
|
+
clobber an existing `.env`).
|
|
20
|
+
4. Runs `db:migrate` to apply the database schema.
|
|
21
|
+
|
|
22
|
+
Run it from your Hogsend project root (the directory with `docker-compose.yml`
|
|
23
|
+
and `.env.example`).
|
|
24
|
+
|
|
25
|
+
## Verify
|
|
26
|
+
|
|
27
|
+
After setup, confirm the instance is healthy and the schema is in sync:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
hogsend doctor --json
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Expect a `ok` verdict with `database` and `redis` components healthy and
|
|
34
|
+
`schema.inSync = true`. If you see `migration_pending`, re-run the migration
|
|
35
|
+
step; if `unreachable`, the API isn't running yet — start it with `pnpm dev`
|
|
36
|
+
(default port 3002), then re-run doctor.
|
|
37
|
+
|
|
38
|
+
## Typical first session
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
hogsend setup # docker up, secret, .env, migrate
|
|
42
|
+
pnpm dev # start the API on :3002 (separate terminal)
|
|
43
|
+
hogsend doctor --json # confirm ok
|
|
44
|
+
hogsend stats --json # sanity-check metrics endpoint
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Notes
|
|
48
|
+
|
|
49
|
+
- `setup` is interactive by design; in a non-interactive / agent context,
|
|
50
|
+
prefer running the underlying steps explicitly and then `hogsend doctor`.
|
|
51
|
+
- The admin key for local reads comes from your `.env` (`ADMIN_API_KEY` or
|
|
52
|
+
`HOGSEND_ADMIN_KEY`); `doctor` itself needs no key.
|