by Autumn
No reviews yetDaily Packaging-division lead enrichment + weekly status doc for Salesforce. Reads PK Leads, researches each one, writes a structured Note to the Lead's Related tab, and publishes a Tuesday status doc to Google Drive. Headless Salesforce UI automation only — no Salesforce REST/Connected-App use.
PK Lead Intelligence is the autumn customer skill that runs the daily enrichment + weekly status pipeline for the Packaging (PK) division. It signs into Salesforce as the human owner via the org's standard Microsoft SSO + TOTP flow, reads PK Leads, researches each one, writes a structured Note back to the Lead's Related tab, and on Tuesday mornings publishes a weekly status doc to Google Drive.
The skill drives the Salesforce Lightning UI through Playwright. It does not use a Salesforce Connected App, the REST API, SOQL, or Apex. Authentication is the same path a person uses.
This document is the operator handoff. Read it end-to-end before running the skill.
The skill ships in phases. Selector verification against HU's live Lightning landed on 2026-05-21 (issue #563); Phases 3/4 are now end-to-end against the production org, not stubbed.
| Phase | Status | What is real |
| :--- | :--- | :--- |
| 1 — auth + storage | ✅ live | Microsoft SSO + 1Password Service Account; storage-state reuse |
| 2 — enrichment dry-run | ✅ live | Lead fetch, Perplexity + Claude + LinkedIn research, .docx render |
| 3 — reporting validation | ✅ live (2026-05-21) | Validate-only navigation to three operator-owned artifacts: All Sources PK Leads report (00OS700000IzEBlMAN), PK Inbound Web Lead and Activity Tracking - SerenAI dashboard (01ZS7000004KhcnMAC), PK Inbound Web Lead and Opportunity Tracking - SerenAI dashboard (01ZS7000004KhePMAS). Spec contracts unit-tested. |
| 4 — live Note write | ✅ live (2026-05-21) | Per-Lead Project Business Unit DOM read (cross-division gate); SerenDB pk_lead_enrichment_log ledger (24h recency); Quill-editor Note-form driver; load-bearing write-then-stamp order; --allow-live × live_mode=true dual gate; weekly doc renderer + Drive share. |
| 5 — cron + slash command | ✅ live (2026-05-21) | scripts/setup_cron.py (daily + weekly local-pull jobs via seren-cron), scripts/run_local_pull_runner.py (claims due ticks, dispatches to agent.py, auto-pauses on publisher 402), scripts/slash/pk_status.py (reads state/weekly_status_runs.jsonl, surfaces latest doc URL or offers on-demand --command weekly run). JSON envelope on --command run and docs/failure_modes.md remain v1 follow-ups. |
Three pieces that shipped differently than the original Phase 3/4 spec, all because the operator's Salesforce permission set in HU is constrained to a regular-user role (no Setup access):
Custom Lead fields were not created. PACKAGING__c,
Last_Enrichment_At__c, and Activity_Gap_Days__c from the
original spec do not exist and never will. The cross-division
gate reads HU's existing Project Business Unit field instead
(value PACKAGING for the PK division). Recency moved to a
SerenDB-owned pk_lead_enrichment_log table because Salesforce
does not need to know when the skill last touched a Lead.
Reports + dashboards are operator-owned, not skill-created. The Lightning Report Builder and Dashboard Builder live inside Aura-app iframes that cannot be cleanly driven every cron tick. The three artifacts above were cloned manually by Nathan; the skill validates each is still reachable on every provision tick but does not edit them.
Per-Lead detail-page read for the division gate. The original
spec assumed the All Sources PK Leads report would surface
PACKAGING__c as a column the cron could read from the list
view. With the field gone, the cron now navigates to each Lead's
detail page to read Project Business Unit directly. One extra
page-load per Lead per cycle is acceptable at the skill's volume.
--command run --dry-run — works end-to-end against a real org login. Produces a .docx of the rendered Note for the first matching Lead and exits.--command run --allow-live — requires inputs.live_mode: true AND inputs.serendb_connection_uri set in config.json. Live runs populate is_packaging from the Lead detail page, then enforce the 24h recency gate via the SerenDB ledger, then drive the Note form on PK Leads only.--command provision --allow-live — navigates to each of the three pinned artifact URLs and confirms they load under the operator's session. Does not edit them.--command weekly — renders the weekly Google Doc and uploads + shares it./pk-status slash command to read this week's status docDo not use this skill to write into divisions other than Packaging. Mis-routed enrichments are a P0 defect — the PK / PL / MD / NW split exists in the source data and the skill respects it.
SEREN_API_KEY and enough SerenBucks to cover
daily Perplexity + Claude calls (~$0.25/run today). See
API Key Setup below — there are three supported
paths depending on where you run the skill.The skill calls five Seren publishers over HTTPS: perplexity (Lead
research), seren-models (Claude hypothesis), google-drive (weekly
doc upload + share), seren-cron (daily / weekly schedules), and
seren-db (SerenDB connection URI for the pk_lead_enrichment_log
ledger). Every one of those calls is authenticated by SEREN_API_KEY
(or API_KEY, which Seren Desktop injects). Pick the path that
matches where you are running the skill:
Seren Desktop. No setup. The desktop runtime injects API_KEY
and the agent can probe mcp__seren-mcp__list_projects to confirm
auth. Skip the rest of this section.
Claude Cowork (the desktop Claude app). Cowork installs custom MCP connectors through its GUI, not a shell command:
https://mcp.serendb.com/mcpThe hosted MCP exposes every publisher this skill calls. No .env
entry is needed — Cowork carries the session for you.
Claude Code (the CLI). Install the same hosted MCP via the
claude command:
claude mcp add --scope user --transport http seren \
https://mcp.serendb.com/mcp
Trigger any MCP call to complete OAuth. If you also want a
SEREN_API_KEY in .env for cron runs, paste the key the MCP
issues:
SEREN_API_KEY=<the-key-the-mcp-handed-back>
No setup — just run the skill (cold-start auto-register). If
none of the above is configured, the skill registers a fresh Seren
agent account on its first publisher call, writes
SEREN_API_KEY=<key> to <skill-root>/.env, and continues. A
one-line warning is emitted on stderr so you can see what
happened. This is the path Claude Cowork users hit when they
activate the skill before setting up the connector — Jill never
sees an error.
Locked-down host with no outbound to /auth/agent either (CI,
air-gapped cron). Register the account from a machine that does
have outbound, then paste the key into <skill-root>/.env:
curl -sS -X POST https://api.serendb.com/auth/agent \
-H 'Content-Type: application/json' \
-d '{"name":"pk-lead-intelligence"}'
Copy .data.agent.api_key from the response — it is shown only
once — into <skill-root>/.env as SEREN_API_KEY=....
Do not create a duplicate account if a key already exists. The
/auth/agentendpoint always issues a fresh$0-balance key; a second account does not inherit your team's SerenBucks. The cold-start auto-register in path 4 only fires when neitherAPI_KEYnorSEREN_API_KEYis set in the environment AND no<skill-root>/.envexists with a key — so an existing team key is never overwritten. Always check<skill-root>/.env(and probemcp__seren-mcp__list_projectswhen MCP is available) before invoking the registration curl manually.
Reference: https://docs.serendb.com/skills.md.
The skill needs your Salesforce username, password, and the TOTP seed backing your MFA rolling code. Pick whichever path fits your setup — the skill tries the env-var path first and falls back to 1Password Business if env vars are unset.
.env (consumer-friendly, no 1Password Business needed)Works on consumer 1Password (Personal/Families), no 1Password at all,
or any other secrets store. The skill reads three env vars and
computes the rolling 6-digit code locally via pyotp. Issue #795.
<skill-root>/.env:
SF_USERNAME=jill@your-company.com
SF_PASSWORD=<your-salesforce-password>
SF_TOTP_SECRET=<base32-seed-from-MFA-setup>
SF_TOTP_SECRET value one of two ways:
pip install -r requirements.txt (pulls pyotp), then
python -c "from scripts.auth.op_service_account import read_salesforce_credentials as r; print(r(vault='', item='').totp_code)"
should print a 6-digit code that matches your authenticator app.Set all three or none. Setting two of three is rejected with an
explicit error — half-set env can otherwise mis-route the SSO
driver if it silently falls through to op.
Use this if your org already runs 1Password Business or Teams and wants secrets centralized. Requires a Business/Teams plan — the consumer Personal and Families plans do not expose Service Accounts.
PK Salesforce Skill
and add one login item named PK Salesforce. The item must carry
username, password, and a TOTP field.op CLI:
brew install --cask 1password-cli (or the Linux package).op --version returns 2.x.OP_SERVICE_ACCOUNT_TOKEN in .env (see Configuration).SF_USERNAME, SF_PASSWORD, and SF_TOTP_SECRET are
not set — the env-var path runs first and would shadow this
one.op vault list must list PK Salesforce Skill.op item get "PK Salesforce" --vault "PK Salesforce Skill" --otp must print a rolling 6-digit code.Never paste the Service Account token into chat or commit it. The
.gitignore blocks .env, but the token is the most sensitive
credential in this skill — protect it like a production password.
cd autumn/pk-lead-intelligence
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
playwright install chromium
cp .env.example .env # fill in
cp config.example.json config.json # adjust
.envEvery variable in .env.example is required. The file is
gitignored.
| Variable | Source | Notes |
| :--- | :--- | :--- |
| SEREN_API_KEY | Seren Desktop or https://docs.serendb.com/skills.md | Used for Perplexity + Claude + Google Drive publisher calls. |
| OP_SERVICE_ACCOUNT_TOKEN | 1Password admin | Read-only SA scoped to the SF vault. |
| OP_VAULT | Hardcoded to PK Salesforce Skill | Rename only if the vault is renamed. |
| OP_ITEM | Hardcoded to PK Salesforce | Rename only if the item is renamed. |
config.jsonThe example in config.example.json ships with all required keys.
The fields the operator typically edits:
| Field | Default | Notes |
| :--- | :--- | :--- |
| inputs.salesforce_org_url | https://<org>.lightning.force.com | Replace with the live org URL. |
| inputs.salesforce_owner_email | empty | The Microsoft / SSO email the skill signs in as. |
| inputs.live_mode | false | Defense-in-depth. Salesforce writes also require --allow-live on the CLI. |
| inputs.monthly_close_target_usd | 500000 | Drives the rolling-forecast pacing math. Adjust quarterly. |
| inputs.google_drive_folder_id | empty | Where the weekly doc lands. |
| inputs.nathan_share_email | empty | Who the weekly doc is auto-shared with. |
| schedule.daily_cron | 0 6 * * 1-5 | Weekdays at 06:00 in schedule.timezone. |
| schedule.weekly_cron | 0 7 * * 2 | Tuesday at 07:00. |
| schedule.timezone | America/New_York | Operator-local. |
| limits.max_leads_per_daily_run | 50 | Hard cap. The skill will not enrich more than this even if the report returns more rows. |
| perplexity.* / claude.* | sensible defaults | Override only if a model is deprecated. |
The populated config.json is gitignored — only config.example.json
is committed.
The default and the only path that should run while the operator is still validating Note quality.
python scripts/agent.py --command run --dry-run
Outputs a local .docx of the rendered Note for the first matching
Lead and exits. Re-run on a fresh Lead until the Note format passes
operator review.
After live_mode=true is set in config.json and the operator
has reviewed at least five dry-run Notes:
python scripts/agent.py --command run --allow-live
Both live_mode=true and --allow-live are required. Either alone
refuses to write. This is intentional — see Pre-Run Checklist below.
Salesforce enforces a ~90-second window between sequential
ContentNote writes on the Lightning UI session. The skill pauses
between Notes in --batch --allow-live by default; without the
pause, batches silently drop Notes after the 2nd–3rd Lead. Override
via inputs.pause_between_notes_seconds in config.json (default
90) or the --pause-between-notes <seconds> CLI flag. Skipped /
failed / non-PK Leads do not trigger a pause — they did not consume
a throttle slot. Set to 0 only when running through a Salesforce
Connected App + REST path (not the default UI flow).
The weekly doc is normally cron-driven, but a manual run is fine:
python scripts/agent.py --command weekly
It refuses to run unless live_mode=true because the doc references
real, written Notes.
Inside a Seren Desktop chat:
/pk-status
Returns the doc URL and the executive summary for the most recent
weekly run. If no doc exists this week yet, offers to trigger an
on-demand --command weekly run.
The skill runs on seren-cron with these defaults (timezone:
America/New_York):
| Job | Cron | What it does |
| :--- | :--- | :--- |
| pk-lead-intelligence-daily | 0 6 * * 1-5 | Enrich up to max_leads_per_daily_run PK Leads; write Notes if live. |
| pk-lead-intelligence-weekly | 0 7 * * 2 | Generate the weekly status doc and share it. |
The schedule lives in seren-cron; a long-lived local pull runner on the always-on host claims due ticks. To register and start the runner:
python scripts/setup_cron.py create --config config.json
python scripts/run_local_pull_runner.py --config config.json
Leave the runner process alive on the always-on host. Closing it is equivalent to pausing the cron.
To pause or resume:
python scripts/setup_cron.py list
python scripts/setup_cron.py pause --job-id <id>
python scripts/setup_cron.py resume --job-id <id>
live_mode=true)Run through this every time the operator enables live writes — at initial cutover and after any extended outage:
op vault list succeeds. The Service Account is reachable.op item get "PK Salesforce" --vault "PK Salesforce Skill" --otp
returns a fresh 6-digit code.python scripts/agent.py --command run --dry-run succeeds end-to-
end on one Lead and produces a clean .docx.config.json has inputs.live_mode = true, monthly_close_target_usd
matches the current target, and google_drive_folder_id +
nathan_share_email are non-empty.If any item fails, do not flip live_mode=true. Fail closed.
If a bad Note format ships to production or the skill starts writing into the wrong division, stop it immediately:
# 1. Pause the cron jobs so no new ticks fire.
python scripts/setup_cron.py list
python scripts/setup_cron.py pause --job-id <daily-job-id>
python scripts/setup_cron.py pause --job-id <weekly-job-id>
# 2. Flip the config gate off so even a manual run cannot write.
# Edit config.json: set "inputs.live_mode": false.
# 3. Stop the local pull runner process.
# Ctrl-C the foreground process, or `kill` it if backgrounded.
These three steps independently block writes. Any one is enough to stop new Notes; the recommendation is all three so a tired operator cannot accidentally undo the stop.
The skill does not auto-delete or auto-rollback Notes that have already been written. If a bad Note batch shipped, the operator manually cleans it up in Salesforce and the local pull runner stays paused until the renderer is fixed and re-validated against dry-runs.
This skill handles customer-confidential CRM data. Read this before operating it.
.gitignore
blocks *.png to keep dev screenshots out of git. Never paste
Salesforce screenshots into chat or share them outside the org —
they always carry PII.inputs.linkedin_scraping_enabled config flag (default false);
when enabled, the skill reads the operator-authenticated
profile page using the same Playwright context as Salesforce
and stores the structured fields it extracts (current title,
tenure, prior roles, education, skills, recent activity). The
scraper never enumerates connections in bulk, never bypasses
LinkedIn's session model, and soft-fails to None on the
signed-out gate so the Note falls through to the existing
not-surfaced markers.enriched_leads row per enrichment so the same Lead is not
re-researched on every run. Rows are kept indefinitely — the
audit trail of what the skill told the human owner about each
Lead is part of the deliverable. Never delete rows from the
ledger; update in place if a Lead is re-enriched.PACKAGING = True.
A mis-routed enrichment that lands a Note on a non-PK Lead is a
P0 defect, not a cosmetic bug.If any of these guarantees is unclear or violated by a code path, treat it as a release-blocking bug. Privacy and division-boundary defects are not "ship and patch later" — they are stop-the-line.
The companion docs/failure_modes.md (ships in phase 5) covers each
of the recurring operational failures and the recovery procedure for
each — Salesforce session expiry, Microsoft Authenticator drift,
Playwright selector rotation, Perplexity / Claude rate limits,
Google Drive sharing failures, SerenBucks depletion, etc. Until that
file lands, escalate non-trivial failures to the implementing
engineer; do not freelance recovery steps that touch live Salesforce
records.
Free
npx skills add serenorg/seren-skillsSelect “Autumn Pk Lead Intelligence” when prompted
openclaw install autumn-pk-lead-intelligenceSee install page for setup instructions
Autumn
Added May 25, 2026