goals-onboarding
Purpose
Interviews the user for risk tolerance, goals, time horizon, and
constraints; ingests current holdings, current liabilities, and checking
spending history; produces an
InvestmentPolicyStatement — the
reference plan every other skill and the Reviewer/Critic reads against —
and captures a LiabilitiesSnapshot,
since debt data isn't derivable from checking transactions alone.
This is the only skill in this project with no path to financial execution. It reads context and writes policy; it never drafts or executes a trade. Keep it that way — don't add tool scopes here that belong to Action Drafting.
When this skill runs
| Trigger | Meaning | Result |
|---|---|---|
initial |
No active IPS exists for this user | Creates version 1 |
life_event |
User reports a change (new goal, changed timeline, etc.) mid-conversation | Creates version N+1 |
drift_review |
Portfolio Analysis reports drift the current IPS bands don't account for, and recommends revisiting | Creates version N+1, pre-filled with prior answers for the user to confirm or change |
The decision to invoke this skill (e.g. noticing a life-event mention) is the root planner's, not this skill's own — this skill only defines what it does once invoked, per ADR-0004.
Inputs
| Field | Source | Required |
|---|---|---|
user_id |
Orchestrator | Yes |
trigger |
Orchestrator (initial | life_event | drift_review) |
Yes |
existing_ips_ref |
Firestore — active IPS for this user, if any | Required when trigger != initial |
HoldingsSnapshot |
Firestore, read-only | Yes — informs feasibility discussion (e.g. a goal the current portfolio can't plausibly reach) |
Existing LiabilitiesSnapshot, if any |
Firestore, read-only | Required when trigger != initial — presented back to the user to confirm/update rather than re-asked from scratch |
| Checking transaction history | BigQuery, read-only, aggregated | Yes — informs liquidity needs (spending patterns, existing reserve) |
W2Document, if any |
Firestore, read-only | Optional — informs income verification and retirement deferral capacity (Box 12 401(k)) |
| Interview responses | User, gathered interactively during the skill's own multi-turn conversation | Yes |
The interview itself is multi-turn and not turn-by-turn specified here — that's an implementation detail. What's specified is the mapping from interview content to IPS fields, below.
Interview → IPS field mapping
Goals. Free-form goal name, target amount, target date — one or more.
Each becomes a goals[] entry. Reality-check against current holdings +
savings rate (derived from checking transaction data); if a goal's target is
implausible given the timeline, flag it to the user during the interview
— don't silently accept or silently block.
Liabilities. Asked directly, one entry per debt: type (credit card,
mortgage, auto loan, student loan, HELOC, other), balance, interest
rate, minimum payment. If an existing LiabilitiesSnapshot was passed in
(revision triggers), present it back for the user to confirm or update
rather than re-asking from scratch. Written as a
LiabilitiesSnapshot —
separately from the IPS, since it's current-state data that changes on
its own schedule (a credit card balance moves monthly; policy doesn't).
Not yet in scope: this data isn't automatically factored into the
risk_tolerance mapping below. High-interest debt arguably should
temper an otherwise-aggressive allocation (risk capacity vs. risk
tolerance) — the field exists so a future Reviewer rule or an explicit
prompt to the user ("you're carrying 22% APR debt — still want an
aggressive allocation?") can use it. Wiring that logic in is a
deliberate next decision, not an oversight to silently paper over here.
Risk tolerance. Not asked directly ("are you conservative or aggressive?") — derived deterministically from two questions, so the mapping is testable rather than a judgment call buried in a prompt:
- Time horizon in years (for the primary goal)
- Reaction to a hypothetical 20% portfolio drawdown:
sell/hold/buy_more
| time_horizon_years | drawdown_reaction | → risk_tolerance |
|---|---|---|
| ≥ 15 | hold or buy_more |
aggressive |
| ≥ 7 | hold or buy_more |
moderate |
| any | sell |
conservative |
| < 7 | hold or buy_more |
moderate |
Time horizon. Directly from the primary goal's target date.
Liquidity needs. reserve_months asked directly; known_upcoming_expenses_usd
summed from user-reported near-term expenses.
Target allocation. Propose default bands per risk tier (below) as a starting point; user can override any band before confirming. Never finalize without an explicit confirmation step — this is the one place in the skill where the interactive interview itself functions as the approval gate, since there's no separate Reviewer/HITL step for policy changes (only for trades).
| risk_tolerance | Default target (min–max band) |
|---|---|
conservative |
equity 30% (20–40), bonds 60% (50–70), cash 10% (5–15) |
moderate |
equity 60% (50–70), bonds 30% (20–40), cash 10% (5–15) |
aggressive |
equity 85% (75–95), bonds 10% (0–20), cash 5% (0–10) |
Constraints. excluded_tickers/excluded_sectors asked directly
(optional, default empty). concentration_limit_percent defaults to 15,
user-overridable. account_type and tax_loss_harvesting_enabled asked
directly.
Approval thresholds. approval_required_above_usd and
approval_required_above_percent — asked directly, with a suggested
default (e.g. $1,000 or 5% of portfolio, whichever is lower) the user can
adjust.
Output
Produces a typed GoalsOnboardingResult
carrying the synthesized interview responses. The trusted orchestrator
validates this structured output and executes the corresponding Firestore
writes:
- One
InvestmentPolicyStatementinstance, written to Firestore, plus - One
LiabilitiesSnapshot, written or overwritten in Firestore (current-state, not versioned — unlike the IPS below, there's no history to preserve here, the latest snapshot simply replaces the prior one).
Versioning invariant (IPS only): exactly one document with
status: "active" per ips_id at any time.
trigger: initial→ write version 1,status: "active".trigger: life_eventordrift_review→ write version N+1 withstatus: "active", and update the previous active version's document:status → "superseded",superseded_by → "{ips_id}:v{N+1}". These two writes happen together — never leave two active versions, or zero.
A summarized version (risk tolerance, goals, key constraints — not the full document) is also pushed by the orchestrator to Agent Platform Memory Bank for semantic recall in future sessions.
Failure mode: incomplete interview
If the user abandons the interview before all required IPS fields are
answered, the Managed Agent does not emit a complete result and no
documents are written — neither the IPS nor the LiabilitiesSnapshot.
Partial state is never persisted: a half-complete IPS would fail schema
validation, and a document that doesn't validate should never exist in
Firestore. Resume from where the user left off on the next invocation rather
than starting over, if practical.
Tools / permissions required
- Managed Agent sandbox: conversational interaction with the user (
RequestInput) - Orchestrator (outside sandbox):
- Firestore: read
holdings, readliabilities, readw2_documents, read/writeips, writeliabilities, writeaudit_log - BigQuery: read
checking_transactions(aggregate queries only) - Agent Platform Memory Bank: write (summarized preferences)
- Firestore: read
- No trade-execution tools, no Alpaca access, no write access inside the sandbox.
Registry metadata
- Registered as:
projects/{project}/locations/{location}/skills/private-goals-onboarding(self-registered skills are namespaced under theprivate-publisher prefix — see ADR-0006) - Skill revision: 0.2.0 (draft — not yet registered)
- Approval scope:
read:holdings,read:liabilities,read:spending,read:ips,read:w2
Acceptance criteria
trigger: initial, no existing IPS → produces exactly one IPS document, version 1,status: active, valid againstips.schema.json, plus oneLiabilitiesSnapshotvalid againstliabilities.schema.jsontrigger: life_eventordrift_reviewwith an existing active IPS → produces version N+1active; previous version flips tosupersededwithsuperseded_byset; exactly one active version exists afterward;LiabilitiesSnapshotis overwritten, not versioned- Interview abandoned mid-way → zero documents written (neither IPS nor
LiabilitiesSnapshot) risk_toleranceis set via the mapping table above for every tested(time_horizon_years, drawdown_reaction)combination — not free-form- Default
target_allocationbands match the risk-tier table and are overridable before the write occurs - On a revision trigger, an existing
LiabilitiesSnapshotis presented back to the user rather than re-collected from scratch - Emits
AuditLogEntryrecords:skill_invokedat start,ips_created(initial) orips_superseded(revision) on write — seeaudit-log-entry.schema.json
Non-goals
The risk-tolerance mapping above is a simplified deterministic heuristic
for demo purposes — it is not a licensed suitability assessment and
shouldn't be presented as one. See project-level non-goals in
00-overview.md.