Extract structured relationship data from incoming messages. Teaches the Switchboard's runtime instance how to identify contacts, interactions, life events, dates, facts, sentiments, gifts, and loans — and produce structured JSON that maps directly to Relationship butler tools.
When the Switchboard classifies an incoming message as relationship-relevant, this skill tells the runtime instance what to look for and how to structure the extraction so the Relationship butler can act on it immediately.
You are not calling tools yourself. You are producing structured JSON
extractions that the Switchboard will forward to the Relationship butler
via route().
Every incoming message may contain zero or more of these 8 signal types. Extract all signals present — a single message can produce multiple extractions.
| # | Signal Type | What to Look For |
|---|---|---|
| 1 | contact | A new person mentioned by name, with optional details (email, phone, role, company, location) |
| 2 | interaction | Evidence of a past or planned meeting, call, text, email, or social encounter with a named person |
| 3 | life_event | Milestone or change: new job, move, engagement, graduation, illness, retirement, promotion, baby |
| 4 | date | Birthday, anniversary, wedding date, or any recurring calendar date tied to a person |
| 5 | fact | A discrete piece of personal information: favorite food, allergy, hobby, pet name, preference |
| 6 | sentiment | Emotional context about a relationship: "I'm worried about Sarah", "Alex and I had a great time" |
| 7 | gift | Gift idea, purchase, or giving event tied to a person and optionally an occasion |
| 8 | loan | Money lent or borrowed, with amount, direction, and optional description |
For each signal detected, produce one JSON object following the schema for that signal type. Return an array of all extractions.
{
"extractions": [
{
"signal_type": "<one of the 8 types>",
"confidence": "HIGH" | "MEDIUM" | "LOW",
"contact_hint": "<name of the person this relates to>",
"data": { ... },
"tool_mapping": {
"tool": "<Relationship butler tool name>",
"args": { ... }
}
}
]
}
Fields:
contact, interaction, life_event, date,
fact, sentiment, gift, loan.contact_id field should be set to null — the Switchboard will resolve
it via contact_search before routing.Detected when a new person is introduced or details about a person are shared for the first time.
{
"signal_type": "contact",
"confidence": "HIGH",
"contact_hint": "Sarah Chen",
"data": {
"name": "Sarah Chen",
"details": {
"email": "[email protected]",
"phone": null,
"company": "Acme Corp",
"role": "engineering manager",
"location": "San Francisco",
"notes": "Met at the conference last week"
}
},
"tool_mapping": {
"tool": "contact_create",
"args": {
"name": "Sarah Chen",
"details": {
"email": "[email protected]",
"company": "Acme Corp",
"role": "engineering manager",
"location": "San Francisco",
"notes": "Met at the conference last week"
}
}
}
}
Tool signature: contact_create(pool, name: str, details: dict | None = None) -> dict
If the contact already exists (resolved via contact_search), use
contact_update instead to merge new details.
Update tool signature: contact_update(pool, contact_id: UUID, **fields) -> dict
name and details keyword arguments.Detected when someone describes a past or upcoming meeting, call, or social encounter with a named person.
{
"signal_type": "interaction",
"confidence": "HIGH",
"contact_hint": "Jake",
"data": {
"type": "coffee",
"summary": "Had coffee with Jake downtown, discussed his startup idea",
"occurred_at": "2026-02-08T15:00:00Z"
},
"tool_mapping": {
"tool": "interaction_log",
"args": {
"contact_id": null,
"type": "coffee",
"summary": "Had coffee with Jake downtown, discussed his startup idea",
"occurred_at": "2026-02-08T15:00:00Z"
}
}
}
Tool signature: interaction_log(pool, contact_id: UUID, type: str, summary: str | None = None, occurred_at: datetime | None = None) -> dict
Common interaction types: call, text, email, coffee, lunch,
dinner, meeting, video_call, party, visit.
If the date/time is not mentioned, omit occurred_at (defaults to now).
Detected when a significant life change is mentioned for a person: new job, move, engagement, baby, graduation, retirement, illness, promotion, etc.
Life events produce two extractions: a note (to record the event) and optionally a date (if a specific date is mentioned).
{
"signal_type": "life_event",
"confidence": "HIGH",
"contact_hint": "Maria",
"data": {
"event": "promotion",
"description": "Maria got promoted to VP of Engineering at her company"
},
"tool_mapping": {
"tool": "note_create",
"args": {
"contact_id": null,
"content": "Life event: Maria got promoted to VP of Engineering at her company",
"emotion": "happy"
}
}
}
Tool signature: note_create(pool, contact_id: UUID, content: str, emotion: str | None = None) -> dict
Emotion values to use for life events: happy, proud, excited,
concerned, sad, neutral.
Detected when a birthday, anniversary, wedding date, or any recurring calendar date is mentioned in connection with a person.
{
"signal_type": "date",
"confidence": "HIGH",
"contact_hint": "Dad",
"data": {
"label": "birthday",
"month": 3,
"day": 15,
"year": 1965
},
"tool_mapping": {
"tool": "date_add",
"args": {
"contact_id": null,
"label": "birthday",
"month": 3,
"day": 15,
"year": 1965
}
}
}
Tool signature: date_add(pool, contact_id: UUID, label: str, month: int, day: int, year: int | None = None) -> dict
Common labels: birthday, anniversary, wedding, graduation,
memorial, name_day.
If only month and day are mentioned, set year to null.
Detected when a discrete, specific piece of personal information is shared about someone: favorite food, allergy, hobby, pet's name, clothing size, preference, etc.
{
"signal_type": "fact",
"confidence": "MEDIUM",
"contact_hint": "Tom",
"data": {
"key": "favorite_cuisine",
"value": "Thai food"
},
"tool_mapping": {
"tool": "fact_set",
"args": {
"contact_id": null,
"key": "favorite_cuisine",
"value": "Thai food"
}
}
}
Tool signature: fact_set(pool, contact_id: UUID, key: str, value: str) -> dict
Use snake_case keys. Common keys: favorite_food, favorite_cuisine,
favorite_color, favorite_drink, allergy, dietary_restriction,
hobby, pet_name, pet_type, clothing_size, shoe_size,
coffee_order, sports_team, music_taste, preferred_language,
nickname, employer, job_title.
Facts are UPSERTed — setting the same key again overwrites the previous value.
Detected when the message conveys emotional context about a relationship or person. This captures the user's feelings, not the contact's.
{
"signal_type": "sentiment",
"confidence": "MEDIUM",
"contact_hint": "Sarah",
"data": {
"emotion": "worried",
"context": "Haven't heard from Sarah in weeks, hope she's doing okay"
},
"tool_mapping": {
"tool": "note_create",
"args": {
"contact_id": null,
"content": "Sentiment: Haven't heard from Sarah in weeks, hope she's doing okay",
"emotion": "worried"
}
}
}
Tool signature: note_create(pool, contact_id: UUID, content: str, emotion: str | None = None) -> dict
Emotion values for sentiments: happy, grateful, excited, proud,
nostalgic, worried, frustrated, sad, guilty, neutral.
Sentiments are stored as notes with an emotion tag. Prefix the content with
"Sentiment: " to distinguish from regular notes.
Detected when a gift idea, purchase, or giving event is mentioned in connection with a person.
{
"signal_type": "gift",
"confidence": "HIGH",
"contact_hint": "Mom",
"data": {
"description": "Silk scarf from that boutique she liked",
"occasion": "birthday"
},
"tool_mapping": {
"tool": "gift_add",
"args": {
"contact_id": null,
"description": "Silk scarf from that boutique she liked",
"occasion": "birthday"
}
}
}
Tool signature: gift_add(pool, contact_id: UUID, description: str, occasion: str | None = None) -> dict
If the gift has already been purchased or given, produce an additional
extraction using gift_update_status (requires the gift ID, which the
Switchboard resolves after creation):
Status tool signature: gift_update_status(pool, gift_id: UUID, status: str) -> dict
Gift pipeline statuses: idea -> purchased -> wrapped -> given -> thanked.
Detected when money lent or borrowed is mentioned between the user and a named person.
{
"signal_type": "loan",
"confidence": "HIGH",
"contact_hint": "Alex",
"data": {
"amount": "50.00",
"direction": "lent",
"description": "Covered Alex's lunch at the Italian place"
},
"tool_mapping": {
"tool": "loan_create",
"args": {
"contact_id": null,
"amount": "50.00",
"direction": "lent",
"description": "Covered Alex's lunch at the Italian place"
}
}
}
Tool signature: loan_create(pool, contact_id: UUID, amount: Decimal, direction: str, description: str | None = None) -> dict
Direction is always from the user's perspective:
"lent" — user gave money to the contact"borrowed" — user received money from the contactAmount should be a decimal string (e.g., "50.00", "1200.50").
If the message mentions repayment/settling, use loan_settle instead:
Settle tool signature: loan_settle(pool, loan_id: UUID) -> dict
Assign a confidence level to each extraction based on how clearly the signal is stated in the message.
The message explicitly states the information with no ambiguity.
| Signal | Example message | Why HIGH |
|---|---|---|
| contact | "I met Sarah Chen, she's an engineering manager at Acme Corp" | Name, role, and company all stated directly |
| interaction | "I had lunch with Jake yesterday" | Explicit interaction type, named person, time reference |
| date | "Mom's birthday is March 15th" | Named person, label, and exact date all stated |
| loan | "I lent Alex $50 for lunch" | Amount, direction, person, and context all explicit |
| gift | "I'm thinking of getting Mom a silk scarf for her birthday" | Item, person, and occasion all stated |
The information is strongly implied but requires minor inference.
| Signal | Example message | Why MEDIUM |
|---|---|---|
| fact | "Jake always orders the pad thai" | Implies favorite dish, but "always" is an inference |
| sentiment | "I should really call Sarah back" | Implies guilt/concern, but no explicit emotion stated |
| life_event | "Did you hear about Maria's new title?" | Implies promotion but details are vague |
| interaction | "I ran into Tom at the store" | Casual encounter — unclear if meaningful interaction |
The information requires significant inference or is mentioned in passing with little context.
| Signal | Example message | Why LOW |
|---|---|---|
| contact | "Some guy named Dave was at the party" | Minimal information, may not be worth tracking |
| fact | "I think Tom mentioned he likes hiking" | Secondhand, uncertain |
| sentiment | "Whatever, it's fine" (about a person) | Extremely vague emotional signal |
| date | "I think her birthday is sometime in March" | No specific day |
Not every message originates from the owner. When the [Source: ...] preamble
identifies a non-owner contact as the sender, signals extracted from the
message must be attributed to the correct person:
| Scenario | contact_hint should be |
|---|---|
| Sender shares their own interest/preference | The sender's name (from preamble) |
| Sender mentions a third person's info | The third person's name |
| Sender recommends something to the owner | The sender's name (it reveals their taste, not the owner's) |
Never attribute a non-owner sender's preferences to the owner. The owner is the recipient, not the subject of the fact.
Source preamble: [Source: Chloe Wong (contact_id: <uuid>, entity_id: <uuid>), via telegram]
Message: "https://reddit.com/r/SingaporeEats/... Good list of places to eat at :P"
Expected extraction:
{
"extractions": [
{
"signal_type": "fact",
"confidence": "MEDIUM",
"contact_hint": "Chloe Wong",
"data": {
"key": "interest",
"value": "food and restaurant recommendation lists; SingaporeEats-style roundups"
},
"tool_mapping": {
"tool": "fact_set",
"args": {
"contact_id": null,
"key": "interest",
"value": "food and restaurant recommendation lists; SingaporeEats-style roundups"
}
}
}
]
}
Wrong: "contact_hint": "Owner" — Chloe shared the link; the interest is hers.
Before creating a new contact or adding data, the Switchboard should search
for existing contacts to avoid duplicates. The contact_hint field is the
primary key for this lookup.
contact_search returns multiple results for a
first-name-only query, set confidence to MEDIUM and include all candidate
names in a "candidates" field:
{
"contact_hint": "Sarah",
"candidates": ["Sarah Chen", "Sarah Miller"],
"confidence": "MEDIUM"
}
contact signal type) combined with whatever other
signal was detected.When an existing contact is found:
contact_update to merge new details into
the existing contact's details JSONB. Do not overwrite existing values
unless the new information is clearly a correction.contact_id and proceed with
the mapped tool.Input message:
"Had dinner with Jake last night. His birthday is April 22nd. He mentioned he's allergic to shellfish. I still owe him $30 from last week. Maybe I should get him a nice bottle of whiskey for his birthday."
Expected output:
{
"extractions": [
{
"signal_type": "interaction",
"confidence": "HIGH",
"contact_hint": "Jake",
"data": {
"type": "dinner",
"summary": "Had dinner with Jake last night",
"occurred_at": "2026-02-08T19:00:00Z"
},
"tool_mapping": {
"tool": "interaction_log",
"args": {
"contact_id": null,
"type": "dinner",
"summary": "Had dinner with Jake last night",
"occurred_at": "2026-02-08T19:00:00Z"
}
}
},
{
"signal_type": "date",
"confidence": "HIGH",
"contact_hint": "Jake",
"data": {
"label": "birthday",
"month": 4,
"day": 22,
"year": null
},
"tool_mapping": {
"tool": "date_add",
"args": {
"contact_id": null,
"label": "birthday",
"month": 4,
"day": 22,
"year": null
}
}
},
{
"signal_type": "fact",
"confidence": "HIGH",
"contact_hint": "Jake",
"data": {
"key": "allergy",
"value": "shellfish"
},
"tool_mapping": {
"tool": "fact_set",
"args": {
"contact_id": null,
"key": "allergy",
"value": "shellfish"
}
}
},
{
"signal_type": "loan",
"confidence": "HIGH",
"contact_hint": "Jake",
"data": {
"amount": "30.00",
"direction": "borrowed",
"description": "Owed from last week"
},
"tool_mapping": {
"tool": "loan_create",
"args": {
"contact_id": null,
"amount": "30.00",
"direction": "borrowed",
"description": "Owed from last week"
}
}
},
{
"signal_type": "gift",
"confidence": "MEDIUM",
"contact_hint": "Jake",
"data": {
"description": "A nice bottle of whiskey",
"occasion": "birthday"
},
"tool_mapping": {
"tool": "gift_add",
"args": {
"contact_id": null,
"description": "A nice bottle of whiskey",
"occasion": "birthday"
}
}
}
]
}
Note: The gift extraction is MEDIUM confidence because "maybe I should get" is tentative, not a definite plan.
Complete mapping of Relationship butler tools used by this skill:
| Tool | Signature | Used For |
|---|---|---|
contact_create | (pool, name: str, details: dict | None) | New contact |
contact_update | (pool, contact_id: UUID, **fields) | Merge details into existing contact |
contact_search | (pool, query: str) | Deduplication lookup |
interaction_log | (pool, contact_id: UUID, type: str, summary: str | None, occurred_at: datetime | None) | Log interactions |
note_create | (pool, contact_id: UUID, content: str, emotion: str | None) | Life events and sentiments |
date_add | (pool, contact_id: UUID, label: str, month: int, day: int, year: int | None) | Important dates |
fact_set | (pool, contact_id: UUID, key: str, value: str) | Quick facts |
gift_add | (pool, contact_id: UUID, description: str, occasion: str | None) | Gift ideas |
gift_update_status | (pool, gift_id: UUID, status: str) | Gift pipeline progression |
loan_create | (pool, contact_id: UUID, amount: Decimal, direction: str, description: str | None) | New loans |
loan_settle | (pool, loan_id: UUID) | Settle existing loans |