Skip to content

u2v-lp-tasks

✍️ Writer · dispatcher-routed · writes to Attio

What it does

u2v-lp-tasks is the smallest writer, and a clean illustration of the whole philosophy. A teammate emails a one-line request — "remind Philipp and me to follow up with Precitec by Friday" — and the automation creates exactly one follow-up task on that existing LP in Attio, with all the named people as assignees. If anything is unclear, it asks rather than guessing. It never invents a task, and it never creates a new LP.

The workflow

flowchart TD
    A["Email: 'follow up with Precitec by Friday, assign Philipp + me'"] --> B["Extract (1 Haiku call):<br/>LP · assignees · due-date phrase · description"]
    B --> C{{"Gate 1: find the LP in Attio"}}
    C -->|0 or many matches| Cx["Reply asking to clarify"]
    C -->|exactly 1| D{{"Gate 2: resolve every assignee"}}
    D -->|any unknown| Dx["Reply naming who couldn't be resolved"]
    D --> E{{"Gate 3: work out the due date"}}
    E -->|unclear| Ex["Reply asking for the date"]
    E --> F["Create ONE task<br/>(all assignees in one array)"]

The router is run_task in pipeline.py: three ordered gates, and a write only if all three pass.

Key decisions

Never guess — three gates, each with a clarifying reply. The LP must resolve to exactly one company (zero or many → ask), every assignee must resolve to a known teammate (any miss → say who couldn't be resolved), and the due date must be unambiguous (unclear → ask). Only then does it write. Notably it does not fall back to the sender as the assignee — defaulting would be a guess.

The model classifies; the code decides. The single Haiku call does one thing: pull the LP reference, the assignee names, the due-date phrase, and the description out of the email — verbatim, resolving nothing. All the consequential work is deterministic: matching the LP and the people, and especially the due-date math in dates.py, which turns "Friday" or "in three days" into an actual date in pure Python (with the reference date passed in, so it's trivially testable). Turning "Friday" into a date is arithmetic — the wrong job for a language model.

One task, many assignees — not a fan-out. Attio supports multiple assignees on a single task, so the automation creates one task with all the workspace members in a single array. No loop, no duplicate tasks — it mirrors Attio's own model.

Read the LP, never create it. The LP lookup is strictly read-only; there is no code path that would create a company. This automation only ever adds a task to something that already exists.

Least privilege — its own keys, exactly like lp-intake: its own scoped Attio and Anthropic credentials, with its own .env overriding any broader inherited key.

Where things live

Path Role
src/u2v_lp_tasks/pipeline.py run_task — the three-gate router
src/u2v_lp_tasks/extract.py The one Haiku call → a structured request
src/u2v_lp_tasks/dates.py Deterministic due-date math (no model, no wall-clock inside)
src/u2v_lp_tasks/owners.py Assignee resolution — matches known teammates only
src/u2v_lp_tasks/attio.py Read-only LP lookup + the single task write
src/u2v_lp_tasks/reply.py Deterministic reply/clarifying-question text
stages/extract_task.md The extraction rubric ("empty beats wrong; never compute a date")
reference/owners.yaml The team roster (name → email + workspace-member id)
reference/attio_field_map.yaml The task write map + LP lookup map
config.yaml The extraction model + body length limit

Knobs a non-engineer can change: the extraction model in config.yaml, and the team roster in reference/owners.yaml (who can be assigned).

Status & how to run it

The task write shape is confirmed live (a disposable smoke test created a task with two assignees + a linked company, read it back, and deleted it), and the pipeline is offline-tested (58 tests, covering every gate and every no-write path). One step remains: a real end-to-end email through the dispatcher.

cd u2v-lp-tasks
PYTHONPATH=src .venv/bin/python -m u2v_lp_tasks.cli preflight
PYTHONPATH=src .venv/bin/python -m u2v_lp_tasks.cli create-task --owner benedikt@u2v.vc --body-file email.txt --json

Go deeper

The due-date parser handles a surprising range of phrasings. Open the repo in Claude Code and ask "show me every due-date form dates.py understands and how the priority order works."