Analysed six Twenty CRM app PRDs (01-06); recommended build order: 06 Migration (HubSpot) → 01 Lead scoring → 03 PostHog → 02 WhatsApp → 05 E-sign → 04 Google.
Researched Twenty marketplace/listing viability. Key findings:
- Marketplace shipped 2026-07-08 (twenty.com/apps). PRDs' "no marketplace" claim is stale.
- SDK is GA at 2.22.0, not alpha. PRDs' alpha-risk sections are stale.
- Public directory is vetted-only (3 first-party apps). Self-service listing PR #22621 still open.
- No developer payment rails.
chargeCredits()meters workspace credits, not app revenue. - Ecosystem = 18 npm packages w/
twenty-appkeyword. All third-party ones are MIT. - SDK is AGPL-3.0, no linking exception, statically bundled into shipped artifact. Closed-source paid tier is legally ambiguous — unresolved.
- Freemium plugin economics weak: 1-3% conversion, needs 10k+ free users / 18-36 months.
Confirmed no migration client lined up. Decided: build 01 Lead Scoring first, defer
06 Migration until a real client + real data exists (06 can't be finished on a sandbox).
See docs/decisions/D-BUILDORDER-lead-scoring-first.md.
Read all six PRDs. Build-now ranking (finishable today × validated demand × fit — distinct from the revenue ranking in decisions.md):
- 03 PostHog — named first-party blocker, 3-5d, same buyer as 06. (fix HogQL injection)
- 01 Lead Scoring — fastest/zero-dep, shiniest SDK, but no demand signal.
- 05 E-sign PandaDoc — clean build, mid demand.
- 02 WhatsApp — top demand but blocked on Meta verification (start paperwork in parallel).
- 06 Migration — only money-validated, but can't finish without a client (the sell).
- 04 Google — weakest demand + monetization; consider not building.
User chose to implement 01 Lead Scoring as a real sales tool for a real rep (not a
demo). Ran interactive /prd; verified GA SDK API via Context7. Wrote
docs/prd/lead-scoring-agent.md (draft, pending approval).
Locked decisions: score both person + opportunity; default list view sorted by score
(native re-sort kept); hybrid execution — single manual create scored inline (seconds),
bulk import → queue drained by cron sweeper (~1 min); fullest enrichment (record +
company + recent activity); cost guards (no-signal skip, cooldown, per-run cap, burst→queue).
Effort revised 2-4d → 5-8d. GA API correction: defineLogicFunction +
databaseEvent/cron trigger settings (old alpha TwentyFunction code was stale).
6 API shapes flagged in Open Questions to verify at scaffold.
Published to Confluence (space SD): hub "Twenty CRM — App Ideas" (page 720897) with the
Lead Scoring PRD as child page 753665. Local docs/prd/lead-scoring-agent.md remains source
of truth; other 5 PRDs will slot in as sibling child pages.
Jira: no Twenty CRM project existed (only GR/KAN/NEHA/PPA/YA — all unrelated). User created project TCA "Twenty CRM Apps" (id 10165, next-gen software, Epic→Story→Task→Subtask). Confirmed via API after a short propagation delay.
Verified capability limits (user challenged): the Atlassian MCP cannot create a Jira project or Confluence space — only issues (in an existing project) and pages (in an existing space). Terminology: Jira = projects, Confluence = spaces.
Ran /breakdown docs/prd/lead-scoring-agent.md in a general-purpose subagent, pinned to
project TCA only, v1 scope only, Open-Questions → spike issues. Awaiting its issue tree to
audit against the PRD (watching for wrong-project leakage, invented scope, broken deps,
missing AC, skill regressions).
Reorg: moved the six idea briefs into per-idea folders under ideas/<n>-<slug>/ (git mv,
staged not committed); added ideas/README.md index. Left docs/prd/lead-scoring-agent.md
in place (subagent reading it). Root now: decisions.md, progress.md, docs/, ideas/.
Decomposed Lead Scoring PRD (v1 only) into Epic TCA-1 + 9 children, all in project TCA (id 10165), correctly parented, Blocks-linked, level-labelled. No leakage to other projects.
Levels: L1 TCA-2 (scaffold) → L2 TCA-3 fields / TCA-4 agent / TCA-5 role / TCA-6 SPIKE (Task) →
L3 TCA-7 enrichment / TCA-8 views → L4 TCA-9 scoreOnEvent / TCA-10 drainQueue.
Open Questions: Q1–Q5 → TCA-6 spike; Q6 (burst tuning) → TCA-9. Phase 2 / out-of-scope excluded.
Skill ran clean — no errors, no wrong-project attempts, no circular deps. Start: /tdd-prep TCA-2.
Independently verified via project = TCA JQL: 10 issues, all parented to TCA-1, correct
types/level-labels, zero leakage. First-run abnormalities to note: (1) breakdown's interactive
approval gates were silently bypassed in the subagent — skill has no non-interactive/auto-approve
affordance; (2) skill's preferred TDD Confluence child-page was skipped (design embedded in Epic);
(3) createJiraIssue.parent param is mis-documented ("for subtasks" but sets Epic parent).
Per "own sibling repo" decision: created ~/AI/Personal/lead-scoring-agent/ (git init, main)
with README (links to PRD, Confluence 753665, Jira TCA-1..10) + node .gitignore. This planning
repo stays docs-only. Scaffolding (create-twenty-app = TCA-2) deferred: no Twenty sandbox yet
— step zero is spinning one up, then auth:login.
Sandbox: https://lead-scoring.twenty.com. Toolchain blocker found: SDK 2.22.0 requires Node
^24.5 + Yarn 4; machine had Node 22 (at /usr/local/bin, shadowing a Homebrew Node 25). Installed
brew node@24 (v24.18.0, keg-only) — use via PATH=/opt/homebrew/opt/node@24/bin:$PATH.
Scaffolded via create-twenty-app@2.22.0 into ~/AI/Personal/lead-scoring-agent (yarn4, node-modules
linker, .nvmrc 24.5.0, own git initial commit). OAuth to the sandbox succeeded during scaffold; the
scaffold's step-6 sync failed transiently but yarn twenty dev --once then synced cleanly (6 objects
added). App live in workspace. README updated with planning links (uncommitted).
Subagent read installed twenty-sdk/twenty-client-sdk types. Findings → docs/decisions/D-TCA-ga-api-shapes.md.
Key wins: (1) native createdBy.source ACTOR flag (IMPORT/MANUAL/API) replaces the
count-based burst detector — simpler/reliable TCA-9, kills Q6; (2) CoreApiClient is genql-only
(query/mutation, new CoreApiClient() from env — PRD's findOne/createOne/context were wrong);
(3) view sorts use fieldMetadataUniversalIdentifier + ViewSortDirection.DESC, ViewKey.INDEX;
(4) activity via timelineActivities/noteTargets/taskTargets; (5) defineField shape confirmed for TCA-3.
Open: Q3 event-name pluralization (person.created vs people.created) — verify live at TCA-9.
8 defineField files under src/fields/ (person + opportunity × leadScore/Summary/ScoredAt/Status),
referencing STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.{person,opportunity}.universalIdentifier; UUIDs in
src/constants/universal-identifiers.ts. yarn twenty dev --once → 8 added, ✓ synced live. Committed +
pushed (app repo main c768b66). Both repos now on GitHub: avisangle/Twenty-CRM + avisangle/lead-scoring-agent
(private). TCA-3 → Done.
Known: 2 pre-existing typecheck errors in scaffold src/__tests__/schema.integration-test.ts
(created.createNote possibly undefined) — not ours; fix pending.
TCA-5: added permissionFlagUniversalIdentifiers: [SystemPermissionFlag.AI] to default-role.ts.
TCA-4: src/agents/lead-scorer.agent.ts (defineAgent, flat {score,summary} responseFormat) +
src/lib/build-prompt.ts (person/opportunity prompt builders) + unit test (4/4 pass). Also fixed the
2 pre-existing scaffold typecheck errors → yarn typecheck now 0 errors. twenty dev --once synced
(agent + rolePermissionFlag). Commits f42afff (TCA-5) + 3793b8b (TCA-4), pushed app repo main.
Design note: agent schema is flat-primitives-only (no min/max) — 0-100 bound lives in field description.
Both → Done.
TCA-7: src/lib/enrich.ts — genql CoreApiClient().query fetching record + linked company +
recent timelineActivities (first 10, DESC). Field names verified vs generated schema. Mocked-client
unit test. build-prompt.ts extended minimally. Commit 3895e04.
TCA-8: src/views/{person,opportunity}-leads.view.ts — INDEX views sorted by leadScore DESC.
Sync plan: 4 add / 1 change (own component) / 0 destroy — no standard view touched. Commit 9274d73.
typecheck 0, unit 7/7. Both → Done.
Caveats to track: (a) standard Company has no size/industry/employees — enrichment uses name/domain/annualRevenue/address (Phase-2: add custom Company fields if scoring needs more); (b) whether the app INDEX view is the nav default is server-side — verify/set in UI (the one PRD "default sort" AC needing a live check).
Remaining v1: TCA-9 scoreOnEvent, TCA-10 drainQueue (both now unblocked).
TCA-9: one DB trigger per function (SDK limit) → 4 thin trigger files → shared src/lib/score-on-event.ts
(no-signal→SKIPPED, createdBy.source IMPORT→QUEUED, else inline scoreRecord). Loop-safe (update triggers
watch business fields only). Commit 2f7c597.
TCA-10: drain-queue.function.ts cron * * * * * → drains QUEUED (cap 20/type) via shared scoreRecord.
FAILED terminal for v1 (no retry-count field — bounded-retry = follow-up). Commit 75328cf.
typecheck 0, unit 14/14, sync ✓ (5 logic functions).
✅ Q3 resolved live: trigger fires on person.created (singular). Test person → SCORED, leadScore=45
in ~20s, then cleaned up. Decision record updated.
Follow-ups filed/pending: TCA-11 (custom Company fields, Phase 2). Possible: retry-count field for bounded retry. Manual acceptance still open: (a) confirm/set the leads view as UI default (sort-by-score AC); (b) scoring-quality gate — sanity-check score spread on cold→hot leads with a real pipeline. All 9 stories (TCA-2..TCA-10) + spike Done. Both repos pushed.
Created 4 test people (title-only, cold→hot), auto-scored live, then deleted (0 remaining, confirmed): Student Intern=8, Marketing Coordinator=22, Director of Sales=45, CRO=60. Strictly ascending, no inversions — scorer behaving correctly. ~4 AI credits. Insight: title-only tops at 60 (agent is conservative on sparse data) → richer scores need company/activity enrichment (TCA-11 + real data). Op note: local ~/.twenty tokens had expired (~40h) and were refreshed; deployed functions use server-side auth so production scoring is unaffected.
v1 DONE + verified. Only manual item left: confirm/set the leads view as UI default (sort-by-score AC).
User checked the frontend and saw no scores. Two real gaps found (lesson saved to memory):
- Existing records unscored — event-driven scoring never ran on pre-install records; no backfill existed.
→ TCA-12:
backfill.function.ts(POST /backfill) +backfill.ts(enqueue null-status → drainQueue scores). Idempotent. Live: scored all 11 existing records (People 45-62, Opps 55-94). Commit 204aff0. - Score columns hidden — views had sorts but no visible
fields. → TCA-8 fix: addedfields(name/leadScore/leadScoreStatus + signal cols). Commit 5bdaf7d. typecheck 0, unit 16/16, sync ✓ (10 viewFields + backfill fn). TCA-12 → Done. Caveat: /backfill HTTP route URL unconfirmed (405 on candidates); backfill ran via direct enqueue — confirm production route URL as follow-up. Lesson: verify on real/existing data + real UI, not synthetic.
Open items
- Confirm with Twenty team whether apps are derivative works of AGPL core.
- Confirm Twenty's native WhatsApp roadmap timing before scheduling 02.
- Trademark consent needed before marketing "for Twenty CRM" (ToS is broad).
User confirmed live in UI: scores visible + sorted DESC in the "by Lead Score" views on real data. v1 shipped.
New Person scored FAILED. Diagnosed (subagent, via replicating the runAgent /metadata call):
"Credits exhausted" — workspace AI credits used up (user confirmed Twenty Cloud trial ended).
NOT a code bug; enrichment fine; systemic (re-trigger also failed, 0 credits consumed). Our agent
has no modelId → workspace-default model, metered by credits. See bug.md.
Fix: restore credits (upgrade Twenty Cloud plan) OR self-host Twenty with own LLM key (no meter).
Filed TCA-13: capture runAgent error + credit-aware re-queue (FAILED records aren't re-scored by
backfill since it filters NULL-status).
Full skill workflow in subagents: tdd-prep (failing tests) → implement (PR #1, 22/22 unit, /simplify)
→ independent review (fresh subagent) → PASS → squash-merged to main (67d086b) → deployed
(twenty dev --once, 6 functions updated in-place). Live behavior now: credit-exhaustion → QUEUED
(auto-retries when credits return, rejected at credit check so no credit spent); genuine error → FAILED
with reason in leadScoreSummary + console.error; backfill re-processes FAILED. Decision record D-TCA-13.
Review minor findings (non-blocking): credit detection is string-match /credits?\s+exhausted/i (fragile
if SDK rewords, but degrades to operator-recoverable); no bounded retry-count yet → TCA-10.
Skill quirks flagged (overhauled skills): (1) new worktree had no node_modules (node-modules linker —
implementer symlinked); (2) initial file writes hit main not the worktree (self-corrected; main verified
clean). Review-side worktree teardown + squash-merge worked correctly.
Still blocked on AI credits (trial ended). Optional: re-queue the 2 FAILED records so they auto-score
when credits return.
Chose public-npm distribution. Prep on app repo: added twenty-app keyword + npm metadata + MIT LICENSE
(commit d38cd4f); yarn twenty dev:build clean (10 files); repo flipped PUBLIC
(github.com/avisangle/lead-scoring-agent). npm name lead-scoring-agent is available; not npm-logged-in.
Publish mechanics (Twenty docs): app:publish → npm (default, public → marketplace) or --private -r <remote>
(tarball → one instance). Final step is user's: npm login → yarn twenty app:publish. Self-hosted
instances surface npm apps via yarn twenty dev:catalog-sync; install via Settings → Applications.
Audited tracked files + full git history: no real secrets — .twenty/.env gitignored, never committed;
real workspace id (bf296e46) absent from repo. One JWT in vitest.config.ts = Twenty's public seed/demo
API key (payload workspaceId 20202020-… seed namespace, scaffold default; only works vs a locally-seeded
instance) — harmless, left as-is. Git-commit email aavi.sangle@gmail.com is public (normal for public repos).
Fixed: removed sandbox URL from README. Added Install section + .claude/ to .gitignore (commit a73d3b6).
Then trimmed the public README: removed "Planning & tracking" (private repo path, Confluence 753665, Jira
keys, stale table), genericized the personal /opt/homebrew/opt/node@24 toolchain note, pointed tests at
test:unit (commit 098aa04). Remaining optional: env-ify the seed JWT in vitest.config.ts (left as-is —
scaffold default, not sensitive).
User ran npm login + yarn twenty app:publish. lead-scoring-agent@0.1.0 is live on npm (MIT,
twenty-app keyword) — https://www.npmjs.com/package/lead-scoring-agent. Installable into any Twenty
workspace (Settings → Applications; self-hosted via dev:catalog-sync). App repo public at
github.com/avisangle/lead-scoring-agent.
01 Lead Scoring: COMPLETE — PRD → breakdown → build (TDD/implement/review) → deployed → verified on real data → hardened → published. Standing: AI credits to run scoring (Cloud plan or self-host+own key); official Cloud marketplace featuring may need Twenty vetting. Next candidate per build-now ranking: 03 PostHog.