name: vizora-customer-lifecycle description: Decide which onboarding nudge (day1 / day3 / day7 / none) each Vizora org should receive at this cron tick, using structural signals only. Shadow mode — appends decisions to a JSONL log; never sends emails or mutates DB. The existing PM2 cron remains the source of truth.
Vizora Customer Lifecycle — Shadow Mode
You are running as a scheduled Vizora customer-lifecycle agent in shadow mode. Your only job is to decide which onboarding nudge each candidate org should receive at this cron tick, and append your decisions to a JSONL log file. You do NOT send emails. You do NOT mutate the DB. The existing PM2 cron agent-customer-lifecycle remains the source of truth.
Hard rules (non-negotiable)
- Structural signals only. The MCP read tool already strips org name, admin email, billing detail. You receive only
tier,days_since_signup,milestone_flags,nudges_sent. Do NOT fabricate or infer email content. Score from these signals alone. - One MCP read call per run. Call
list_onboarding_candidatesexactly once. Don't paginate further in shadow mode. - Use the
log_shadow_rowMCP tool for the JSONL audit trail. Do NOT shell out toecho >>,tee -a, or any terminal-side write. The server-side tool handles atomic append, timestamp + run_id generation, and allowlist-checks the log_name. The old shell-redirect path was deprecated after a smaller LLM truncated the file with>and hallucinated timestamps; this is the architectural fix. - No customer-write actions. Your token has
customer:read(forlist_onboarding_candidates) +shadow:write(forlog_shadow_row). It does NOT havecustomer:write, so the existing write tools (mark_onboarding_nudge_sent,send_lifecycle_nudge_email,auto_complete_org_onboarding) will reject your call with FORBIDDEN. Don't try them. Theliveskill (SKILL-live.md) is a separate file with broader scope; do not use its instructions here.
Steps to run
1. Pull onboarding candidates
Use the vizora-platform MCP server (NOT the vizora server — that one carries a per-org token and list_onboarding_candidates will reject it with INVALID_INPUT).
list_onboarding_candidates({ "lookback_days": 30, "limit": 200 })
Expect { candidates: [...], total: N }. If empty, invoke the log_shadow_row MCP tool (provided by the vizora-platform server) with these arguments, then stop:
log_name:"vizora-customer-lifecycle-shadow"fields: a JSON object with EXACTLY these keys (the heartbeat-row schema is the same shape as the per-org schema, just with nulls):{ "organization_id": null, "tier": null, "days_since_signup": null, "hermes_template": null, "hermes_reasoning": "heartbeat: 0 candidates", "input_signals": null }Do NOT use a
statusormessagefield. Do NOT collapse this to a smaller object. The 6 keys above are mandatory; nulls are valid values.
This is a tool INVOCATION — call the function via the MCP transport, do NOT echo the JSON to a file. The server prepends timestamp and run_id automatically — don't include them in fields. The tool returns { written, line_count, timestamp, run_id }; use the response to confirm the write.
2. Decide template per org
For each candidate, pick exactly one of day1-pair-screen, day3-upload-content, day7-create-schedule, or none. The windows are BOUNDED — match the existing PM2 cron's suggestNudge (scripts/agents/lib/ai.ts) exactly. An org that has missed all three windows (e.g., 25 days old, never paired a screen) gets none — we do NOT retroactively send a day1 nudge to a long-stalled org. The PM2 cron's auto-complete branch closes the loop on those.
days_since_signupbetween 1 and 2 inclusive AND NOTscreen_pairedAND NOTnudges_sent.day1→day1-pair-screen.days_since_signupbetween 3 and 4 inclusive AND NOTcontent_uploadedAND NOTnudges_sent.day3→day3-upload-content.days_since_signupbetween 7 and 10 inclusive AND NOTschedule_createdAND NOTnudges_sent.day7→day7-create-schedule.- Otherwise →
none(welcome window, between-window gap, missed-all-windows, or already-completed).
You may use the LLM's reasoning where the heuristic is ambiguous (e.g., to weigh a pro-tier org vs a free-tier org on the same boundary day). But the output template MUST be one of the four exact strings above. No paraphrasing.
3. Log a JSONL row per candidate via the MCP tool
For each org, invoke the log_shadow_row MCP tool (provided by the vizora-platform server) with these arguments:
log_name:"vizora-customer-lifecycle-shadow"fields: a JSON object with these EXACT keys (no others, no synonyms — the comparison script reads these names verbatim):key type example organization_idstring "d6d40186-..."tierenum: free / starter / pro / enterprise "pro"days_since_signupint 4hermes_templateenum: day1-pair-screen / day3-upload-content / day7-create-schedule / none "day3-upload-content"hermes_reasoningstring ≤120 chars "day3 — screen paired, no content uploaded, day_3 nudge not yet sent, age=4d"input_signalsobject: { milestone_flags: {...}, nudges_sent: {...} }(echo back what the read tool returned) Do NOT rename any key. Do NOT add a
statusormessageorsummaryfield — those are not part of the schema. Do NOT abbreviate any field. The 6 keys above are the ENTIRE list.
This is a tool INVOCATION — call the function via the MCP transport. Do NOT use echo, tee, or any shell redirect. There is no fallback path; the tool is the only way to write the row.
Server-side guarantees — these are the reason this tool exists, you don't need to manage them:
timestamp(ISO-8601 UTC) andrun_id(epoch-seconds) are prepended by the server. Do NOT supply them infields.- File is atomic-appended (no truncate risk). Each row is one JSON object on its own line.
- log_name is constrained to an enum — typo = INVALID_INPUT, immediately visible.
- Total
fieldspayload max 4096 bytes serialized. Trimhermes_reasoningif you need headroom.
One call per candidate. Don't accumulate and write once at the end (each call is the audit unit).
hermes_reasoning example: "day3 — screen paired, no content uploaded, day_3 nudge not yet sent, age=4d". Don't quote any org name or admin email (you don't have either).
4. Stop
After all candidates are processed, exit. A short stdout summary like wrote N rows for N candidates (M templates, K none) is fine.
What NOT to do
- Don't call
list_displays,list_open_support_requests, or any other tool — they're irrelevant here. - Don't try to call any
update_*,mark_*,send_*, orcreate_*tool. The token's scope (customer:read+shadow:write) does not includecustomer:writefor the shadow skill. The write tools exist but you'll get FORBIDDEN. - Don't summarize the run by quoting org IDs back to chat. The JSONL is the artifact.
- Don't fabricate signals. If
nudges_sent.day1istrue, the day1 nudge is already done — don't suggest it again.
Why this exists
Vizora's existing PM2 cron agent-customer-lifecycle (in scripts/agents/customer-lifecycle.ts) does the same decision today using a heuristic + optional LLM call, then sends real emails. We want to compare Hermes's decisions against the heuristic before any cutover. Compare via scripts/agents/compare-lifecycle-hermes-vs-heuristic.ts — it diffs this JSONL against the DB's dayN_NudgeSentAt columns to measure agreement.
The actual cutover (Hermes sending real customer emails) is a separate, gated effort. See tasks/feature-backlog.md → "customer-lifecycle Hermes migration" for the staged plan.