Changelog¶
Changelog¶
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[2.0.7] - 2026-11-03¶
Exception to semantic versioning (as 2.0.2–2.0.6): two fixes change which rows a filter returns. Each is marked Behavior change.
Highlights¶
A repeated --filter now combines its conditions instead of keeping only the last one, and a
person or company field filtered by id (Owner = 26229794) matches that person instead of
nothing.
Changed¶
- Behavior change (correctness fix). CLI: repeated
--filtervalues are ANDed onlist export,company lsandperson ls, and so are repeatedlist export --expand-filtervalues. Only the last one used to apply, and the others were dropped without a warning, so such runs returned rows that didn't match the earlier conditions: re-run them.--filter A --filter Bis(A) & (B); for OR, use|inside one value. Each repeated value must be a complete expression on its own. (The MCP server refused a repeated--filter, so MCP calls weren't affected.) - Behavior change (correctness fix). CLI and SDK: a person or company field compared with a
number matches the record's id.
Owner = 26229794(andin,has_any,has_all) in--filter,queryandaffinity.filterscompared the name with "26229794" and matched nothing, without a warning (!=matched every row). It now matches that person's id, and names still match as before. Other record references, such as lists, match their id too. Re-run filters that used ids. A reference with no name (a person without a first and last name, a company with an empty name) still can't be matched by id.
[2.0.6] - 2026-10-14¶
Fixed¶
FieldResolver.get,get_manyandget_by_idaccept a plainListEntry(as returned bycompanies.get_list_entries()andpersons.get_list_entries()) in their type hints. They already worked on it at runtime; strict type checkers no longer need a cast.
[2.0.5] - 2026-10-14¶
Exception to semantic versioning (as 2.0.2–2.0.4): two fixes change what a call returns or what the CLI prints. Each is marked Behavior change.
Changed¶
- Behavior change. CLI: deleted dropdown options in
field historyandfield history-bulk(--json). Affinity sends an option deleted since as its bare text ("Reach Out") and a live one as{id, text, rank, color}; the CLI passed both through, so one field's history mixed strings and objects. A deleted option is now{"id": null, "text": "Reach Out", "deleted": true}, so every dropdown value has atext. Text fields keep their string values; live options are unchanged. When a value is plain text the CLI reads the field's metadata (one call) to tell a dropdown from a text field (history-bulk --list-idalready has it). Table and CSV output show the text, as before. - Behavior change (correctness fix). SDK:
on_error="skip"leaves failed records out ofAsyncReminderService.list_batchandAsyncFileService.list_batch. A failed request used to leave its ids in the result with empty lists, which looked like "no reminders" / "no files"; one failed reminders request (50 ids) silently hid them all. A failure on a later page left a partial list that looked complete. They are now absent from the result, as with every otheron_error="skip"method: a requested id with no key failed. If you stored or cached results of these calls made withon_error="skip"before 2.0.5, some empty or partial lists may be failures: re-fetch them. - SDK:
field_value_changes.iter_all()reads 10,000 changes per call by default (was 100), the V1 maximum: a 16,000-change history takes 3 calls instead of 161 (paging ends on an empty page). Passpage_sizefor smaller pages. - SDK:
AsyncCompanyService.batch_get/AsyncPersonService.batch_getwithwith_interaction_datesandfield_ids/field_typesread the fields in one call per 100 ids and the dates in one V1 call per record: about N + N/100 calls instead of 2N. Results are the same. Underon_error="skip"a failed fields call falls back to one read per id of it.
Added¶
- CLI:
list export --filter,company ls --filterandperson ls --filterreport how the filter was split inmeta.serverFilter(--json):pushed(conditions Affinity's search applied),local(checked only locally, with reasons) and, onlist exportwhen the search was refused or turned off,fallback(why: refused, the list's fields unreadable, orXAFFINITY_QUERY_SERVER_FILTER=0; the whole filter then ran locally).list export --dry-runshows it asdata.serverFilter. A--cursorresume doesn't report it. Likequery --include-meta, but always present with--json. - SDK:
persons.get_list_entries()takeslimitandcursor(it returned only the first page).
Fixed¶
- SDK:
companies.get_list_entries()andpersons.get_list_entries()set each entry'sentity_idandentity_type(ORGANIZATION/PERSON); the endpoint doesn't send them, so they wereNone.entitystaysNonethere. Acursorfrom another company's or person's entries raisesValueErrorinstead of labelling its entries with the wrong record. CLI:company get --expand list-entriesandperson get --expand list-entries(--json) now show each entry'sentityIdandentityType(1 company, 0 person). - SDK: the
ListEntrydocstring said it always contains the entity; it says whereentityis filled. - Docs: filtered-run cursors start with
xsearch2:(the CLI page, filtering guide, CLI skill and MCP data model saidxsearch1:). Thefield changesdocs say a deleted dropdown option is{"referenceType": "deleted-entity", "displayValue": ...}. The companies and persons pages say what reading interaction dates for many records costs, and the companies page andbatch_getdocstring point to the relationship-intelligence fields for last / next meeting dates.
[2.0.4] - 2026-10-14¶
Exception to semantic versioning (the maintainer's call, as 2.0.2 and 2.0.3): which API key
the CLI uses changes when a key flag and a key environment variable are both set. The
### Changed entry says how. Some fixes below also change what a call returns or raises,
or a CLI exit code; each is marked Behavior change.
Changed¶
- CLI:
--api-key-fileand--api-key-stdinwin over the environment. A key named on the command line is used even whenAFFINITY_API_KEY,AFFINITY_API_KEY_FILEorAFFINITY_API_KEY_COMMANDis set, and the CLI warns that those are ignored (the file is not read and the command is not run). Before, an environment key silently replaced the flag's key, so a staleexportcould send reads and writes to another workspace. To keep using the environment key, drop the flag. The SDK and the MCP server (which passes no key flag) are unchanged.
Fixed¶
config check-keyreportssource: "flag"(and apatternrepeating the flag) when--api-key-fileor--api-key-stdinis given, as commands now use that key first. It doesn't read stdin; a missing or empty key file counts as not configured.- The missing-API-key error no longer says
config setup-keysaves to the system keychain: it writes a config file, or a.envfile with--scope project. - Docs: one API-key resolution order across the README, guides, CLI page, MCP README and CLI
skill (there is no
--api-keyCLI flag). - Docs: the README coverage table, routing guide, glossary and errors guide describe 2.0's
routing (company and person field values,
notes.get, transcripts, teams, semantic search and connections are V2);api-versions.mdlists every call that sends a minimum version; beta endpoints need no opt-in. - Docs: the CLI command page documents
files ls/read/downloadon persons, companies and opportunities (not the oldfiles dump),relationship-strength ls(notget),opportunity field,list entry get,file-urlandconfig update-check; the CLI page says which list commands return a baredataarray. - Docs: the official-MCP comparison lists this project's transcript, meeting, team, user,
duplicate and connection reads;
VERSIONING.mddescribes 2.x and its patch-release exceptions; the MCP README asks for Python 3.10+. - Docs: the docs home page introduces the project and links configuration, migration, API
versions, the query command and webhooks; a Transcripts reference page; the reference nav no
longer files V2 services under "V1 endpoints"; the
Affinity/AsyncAffinitydocstrings and the sync/async table list every service; the field-values guide starts with the V2 company/person/list-entry methods; the plugins page covers all three plugins. - Docs and
file-urlhelp: examples use placeholder ids and names. - Behavior change. SDK: an empty id list returns an empty page.
list(ids=[])on companies, persons, opportunities, webhooks, teams, users and reminders used to omit the filter and return the whole collection; it now returns no rows without calling the API.limit=0(or less) raisesValueErrorwhere it used to be dropped silently (companies, persons, opportunities, a company's lists and list entries, saved-view entries). - Behavior change. SDK: field writes. A
boolvalue raisesTypeError(Affinity has no boolean field type; it was sent as malformed text). Datetimes are sent as whole-second UTC withZ, like every other write; a naive datetime is taken as UTC. - Behavior change. SDK: delete results. A delete whose response has no
successkey (an empty body) returnsTrue; only an explicit"success": falsereturnsFalse. - SDK:
get_all_pagesraisesAffinityErrorwhen it stops atmax_pageswith pages left, instead of returning (and caching) a cut-off result. - SDK: company and person merges raise
AffinityErrorwhen the response has notaskUrl, instead of returning an empty URL. - SDK: the sync client rejects an
asynchook withConfigurationErrorat construction; underhook_error_policy="swallow"it was logged and never ran. - SDK: followed pagination URLs must match the exact host and port of the V1 or V2 base URL.
- SDK: the response cache key includes the per-call API version sent.
- SDK:
tasks.waitno longer sleeps past itstimeout. - SDK:
lists.resolveno longer remembers a name it didn't find, so a list created later is found. - SDK:
lists.get_sizeis shared by every list service on a client and dropped when an entry is added or removed, an opportunity is created or deleted, or a person or company is deleted. - SDK: paging stops when a cursor repeats, when a page says more follow but gives no cursor, or
after 1,000 empty pages in a row (with a
RuntimeWarning), instead of looping. This covers the page iterators (iter(),all()) and thepages()generators of companies, persons, opportunities, lists, saved views and list entries (filtered reads included), sync and async. - SDK: association paging tolerates
"pagination": null. - SDK filters:
in,has_anyandhas_allcompare values the way=does, soscore in [5, 6]matches the number 5. - SDK models:
FieldId(True),FieldValueType(True)and fractional floats for open enums raiseValueError; a fractional float no longer replaces a known enum member for the rest of the process.FieldValues.getaccepts bare-digit ids (123,"123"). Person and opportunity company ids also readcompanyIdsandcurrent_organization_ids;LocationreadsstreetAddress; V2 list entries pick the entity model fromtype. - CLI
query: resuming a cut-off result no longer skips rows. The resume cursor jumped to the next API page, losing the rest of the current page and every fetched row that didn't fit in--max-output-bytes. A cut-off result, with its included records, is now cached and the cursor resumes from it without API calls; each page'sincludedholds only the records related to that page's rows, also whenselectleaves outid(TOON pages hold as many rows as fit, with matching counts; related records that can't be matched to rows are cut to the budget and counted inincludedOmitted). A cursor needs the same--max-recordsas the first run; a cursor from an earlier version is refused as not matching the query (cursors expire after an hour). - Behavior change. CLI
query: errors are no longer read as "no data". A rate-limit, server or auth error while fetching related records (forinclude, relationship filters, or an id lookup, orinclude: interactions), an expansion (interactionDates,unreplied, person names) or a list's field definitions (when the query uses a list field) fails the query. Before, the record silently had no related records, an id-only stub or an empty expansion, sonone,alland_count eq 0filters could match wrongly. Only a deleted or merged record counts as empty. - Behavior change. CLI
query:--max-records. When results are cut at it, a warning says so andmeta.truncatedis set, on every path (before, the cut was silent); withorderByor an aggregate the cut comes after sorting and aggregating (also when the query has nowhere), so counts, sums, groups and top-N use every match. Before, they were computed on the first 10,000. - Behavior change. CLI
query: Ctrl+C exits 130 (exit 0 before). Rows that already went through the query's filter, sort and aggregate are printed; when one of those hadn't run yet, no rows are printed (before, unfiltered rows were) and the error type isinterrupted. On Python 3.10, Ctrl+C prints no rows (it still exits 130); Python 3.11+ prints them. - Behavior change. CLI
query: alistNamethat matches more than one list, or none, is a validation error (exit 2) asking for the list id; before, the last list with that name was used. An include'slist:name that matches several lists fails (exit 2,ambiguous_resolution) and one that matches none is not found (exit 4); before, the scope was dropped.min/maxover objects, lists, or a mix of numbers and text is a validation error instead of a crash or a silent pick. - CLI
query: withorderByorlimit,include(and anexpandthe sort doesn't read) runs on the rows kept after sorting, limiting and--max-records, not on every match, so a top-N query makes one call per kept row. - CLI
query: an Affinity error exits with the CLI's documented code (3 auth, 4 not found, 5 rate limit or server error) and its usual error envelope, instead of exit 1 with "Query execution failed". - CLI
query: CSV output with--max-output-bytescuts on whole records (a note with a line break could be split), has every row's columns, honors--csv-bomand can be resumed; JSONL resume no longer skips rows whose data contains"truncated". A row larger than--max-output-bytesis printed whole (JSON too, which exited 1 partway through paging) instead of giving an empty page whose cursor repeats. - CLI
query:includeoptions are applied:list,where,limitanddayson alistEntriesinclude, andlimitanddayson aninteractionsinclude. Before, they were ignored, so related opportunities came from every list and interactions used the default 90-day window and 100-record limit. - CLI
query: a relationship filter oninteractions(interactions._count,exists/all/none) no longer hangs when the query has at least as many records as the concurrency limit (10). - CLI
query: when fetching related records for alistEntriesinclude fails, the other fetches are stopped instead of continuing to call the API and printing "Task exception was never retrieved". - CLI
query --output toon: a cut-off page, itscursor:line included, stays within--max-output-bytes(if the cursor alone, about 500 bytes, doesn't fit, one row is shown). - Behavior change. CLI
query --output csv: without--max-output-bytes, execution warnings (the--max-recordscut, multi-value fields cut off) go to stderr as with the other formats, and the CSV has the same shape as with a budget: columns inselectorder (first-seen order withoutselect; they were sorted alphabetically), selectedfields.*and expandedinteractionDatesflattened into columns, and every record ending with CRLF. - CLI
query:groupByon object values (such as a location) works; formula fields filter and sort as numbers. - Behavior change. CLI: a delete Affinity reports as failed exits 1 (
api_error) instead of printing"success": falsewith exit 0. - Behavior change. CLI
field history-bulk: when every entry fails it exits with the usual code for that error (3 auth, 4 not found, 5 rate limit or server; 1 for mixed errors), and 3 on any auth error (all exited 0). Partial failures still exit 0 with warnings and now setmeta.truncatedwithtruncationReason: "partialFailure". - Behavior change. CLI
list exportexpansions: a failed interaction, opportunity or unreplied-message lookup fails the export under the default--expand-on-error raise(it was left empty silently), and warns underskip, keeping the entry's other expansions. An auth error during any expansion fails the export; a deleted opportunity is left out. With--csvthese warnings, and the export's other warnings (such as "Filtering locally"), are printed to stderr; they were dropped. - Behavior change. CLI
--page-sizebelow 1 is a usage error (exit 2) oncompany,person,opportunity,list,note,interactionandreminder ls,list exportandfiles download. With the SDK's newlimitcheck it was aninternal_error(exit 1) on most of them, andnote lsandinteraction lsaccepted it.--help --json(and the MCP command registry) reports such range-checked options, includingsearch --max-resultsandfiles ls --max-results, asint; they werestring. - Behavior change. CLI field writes:
true/falseanywhere in a--set-jsonvalue (including list items and object values) is refused before any write (exit 2,validation_error); Affinity has no boolean field type. A text field no longer storestrueas"true". - CLI
list entry field --unset-valueexits 1 when Affinity reports the delete failed, instead of reporting the value as deleted. When an--unset-valueor--appendstep fails after something was already changed, the error ispartial_writenaming every field written and value removed; a step that timed out or lost the connection is named as "may or may not have been applied" (details.uncertain), and alone it is anetwork_errorthat says re-running is safe;field deleteclears its caches only after a confirmed delete. - CLI: answering "n" to a confirmation under
--jsonprints an error envelope (typeaborted), still exit 1, instead of an empty stdout. - CLI
list ls --type: warns when the first page has no list of that type but more pages exist.
Plugins¶
- CLI plugin: 2.0.2 → 2.0.3 — skill: the API-key resolution order (key flags first) and
where
config setup-keystores the key. Installs pin the CLI version the plugin was written for (XAFFINITY_CLI_SPECoverrides it); the Read hook also guards the CLI'sconfig.toml; the skill names thepartialFailuretruncation reason; the file download example uses--out(there is no--output-dir). - SDK plugin: 2.0.3 → 2.0.4 — skill: the duplicate-company example uses a placeholder name.
[2.0.3] - 2026-10-13¶
Exception to semantic versioning (the maintainer's call, as 2.0.2): some results change
(list(filter=...) pages, the local sort's order, date comparisons in filters). The
### Changed entries say how.
Changed¶
- SDK list-entry filters go through Affinity's search:
lists.entries(id).list/pages/all/iter/get_first(filter=...)send the part of the filter Affinity's search can answer and check the whole filter locally, aslist export --filterdoes. Only when no part can go there (or the search is refused) is every entry read; that case emits the newClientSideFilterWarning(aUserWarning). list(filter=...)returns the matches of one page read, possibly fewer thanlimit(or none), with anext_cursorthat carries the filter: follow it, or useiter()for every match. Before, it filtered only the first page and had no way to continue.- Filtered-read cursors are
xsearch2:(the filter travels as data, so multi-word field names and dates round-trip) in the SDK,list export, andcompany ls/person ls; 2.0.2'sxsearch1:cursors still work. - Field values can be named by id in text filters (
F.field("field-123"),--filter 'field-123 = …'), asqueryallowsfields.<id>. query's local sort follows Affinity's order (when Affinity can't sort, e.g. an unsortable field or a refused search): ranked dropdowns sort by rank (not by text), multi-value fields by their first item, and text case-insensitively ("apple" before "Zeta"); empty values stay last in both directions. Fields named only inorderByare now fetched (a local sort used to see them empty). Grouped rows (groupBy) sort by their keys as before.- Quantifier queries (
all/none/exists) on companies and persons no longer always need--max-records: theirwheremust test a field, which Affinity's search applies first; when it matches at most 100 records the query runs, otherwise it stops and names the count.--dry-rungives a number of API calls instead of "UNBOUNDED" (with--max-recordstoo), which also fixes the MCP query tool's timeout for these queries (it fell back to 120 s). Validation errors found while the query runs exit 2 withvalidation_error, like those found while planning.
Added¶
server_filter=Falseon the list-entrylist/pages/all/iter/get_firstreads every entry and filters locally (to compare or debug), andaffinity.ClientSideFilterWarning.-
More filters go to Affinity's search (verified live):
starts_wordon single-company fields, andis_not_null/is_nullon location fields. -
A team's members and accessible lists, every page (V2, beta; API 2026-09-17+):
teams.list|iter_members(team_id)andteams.list|iter_accessible_lists(team_id); CLIteam members <id>andteam lists <id>(team getshows only the first 10).
Fixed¶
- Dates compare as dates in
>,>=,<,<=and between, everywhere filters run (query,--filter,--expand-filter,company ls/person ls --filter, and the SDK'saffinity.filters): relative dates (-30d,today) and ISO dates are compared with the field's timestamp in UTC. Before, the text filter compared them as text, soClose > "-30d"matched every dated entry. Equality stays textual (= "today"matches the word). Timestamps withZor long fractions parse on Python 3.10 too. list export --field X --filter "Y = …"returned no rows: the filter's fields are now fetched even when--fieldleaves them out of the output.TeamreadsupdatedAt(updated_at), and accepts themembersPreview/accessibleListsPreviewblocks under their own names too.
[2.0.2] - 2026-10-13¶
Changed (breaking)¶
Exception to semantic versioning: these change what some filters return, in a patch release
(the maintainer's call). Filters now mean what Affinity's own search means, so the same filter
gives the same answer whether Affinity or the CLI applies it. This reaches Python code too:
affinity.filters (F, filter strings, lists.entries(...).list/iter/pages(filter=...))
uses the same comparison rules as the CLI.
- An empty multi-value field counts as empty.
is_null/is emptynow match a field with no items ([]), andis_not_null/is not nullno longer do, inquery,--filterand the SDK'saffinity.filters. containson person and company fields is refused (inqueryon list entries): use the newstarts_wordoperator, which matches a word in the name that starts with the text, the way Affinity's search does ("Jan" finds "Jane Doe"; "ane" doesn't).querysorts the way Affinity's search sorts: Affinity's order is kept when it sorts, and the local sort breaks ties by id (ascending) instead of keeping fetch order.query: a field name two list fields share is refused. Name the field by id instead:fields.<field id>(e.g. Affinity's and Dealroom's "Year Founded"). Such fields come back keyed by id, instead of one silently overwriting the other. Any field can be named by id.- Person and company field values filter by name in
--filterand the SDK'saffinity.filters(Owner = "Jane Doe",Owner starts_word "Jan"), as they already did inquery; before, they were compared as raw objects.
Added¶
-
starts_wordfilter operator (queryJSON and--filtertext:Owner starts_word "Jan"). -
Teams, users and feedback (V2):
client.teams.list/iter/get(Affinity API 2026-09-17+; filter by name, sort,includesfor the first members and accessible lists),client.users.list/iter/get(internal users;term,status; a user's id is their person id),client.feedback.send(kind, subject, body)(to Affinity's product team). New modelsTeam,TeamMember,UserAccount,UserStatus. interactions.get_meeting(id)(V2, API 2026-07-15+) andnotes.list/iter_attached_companies|persons|opportunities(note_id)(every entity a note is attached to, beyondget()'s preview).- Duplicate suggestions and warm intros (V2, beta):
companies/persons.list|iter_duplicate_suggestions(API 2026-07-15+;term,match_criteria,order_by) andcompanies.list|iter_coworker_connections(company_id)/list|iter_investor_executive_connections(company_id)(people at a company today whom someone you know may know). CLIcompany duplicates,person duplicates,company connections. - One field at a time (V2):
companies/persons.get_field_value(id, field_id)(the value, unwrapped) andupdate_field_value(id, field_id, value, value_type=None);companies/persons.list|iter_field_value_items(id, field_id)andlists.entries(list_id).list|iter_field_value_items(entry_id, field_id)page through a multi-value field's items (API 2026-09-17+);lists.entries(list_id).list|iter_field_changes( entry_id, ...)give one entry's field change history, oldest first. - Search on Affinity's side (V2, beta):
companies.find,persons.findandlists.entries(list_id).find(plusiter_find, andfind_bodyfor a prebuilt request body) filter, sort and keyword-search on the server, so only matching records are downloaded. Build filters withaffinity.criteria:Condition(field_id, operator, value),all_of,any_of,Sort. Value types are read from the fields metadata when omitted, and each operator is checked against the field. Needs the "Export All Organizations directory" permission (companies, persons) or "Export data from Lists" (list entries). Allowed under read-only mode. queryon list entries filters on Affinity's side: conditions on list fields that Affinity's search answers (dropdown and ranked-dropdown values,!=on dropdowns, numbers, dates, textcontains/eq/starts_with,is_not_nullon single-value fields) go to the server, so only matching entries are downloaded; the fullwherestill runs locally on what comes back, so results are unchanged. Everything else (related records,entityName,is_null, Affinity's enriched fields, field names two fields share) is filtered locally, as before. If the search is refused (for example, no "Export data from Lists" permission) the query reads the whole list instead, with a warning.--include-metashowsmeta.serverFilter(what went to the server, and why the rest didn't);--dry-runnames the candidate fields.XAFFINITY_QUERY_SERVER_FILTER=0turns it off.list export --filterfilters on Affinity's side through the same engine asquery: the--filtertext becomes aquerywhere clause, the part Affinity's search answers is sent, and the whole filter still runs locally. When Affinity applies the whole filter,--all/--max-results/--first-page-onlyare no longer required (page one is the first page of matches); otherwise the error'sdetails.localsays which conditions stay local. The printed cursor (xsearch1:...) carries the search, so--cursoralone resumes a filtered export. A refused search falls back to reading the whole list, with a warning.company ls --filterandperson ls --filterwork (they used to be refused): the filter goes to Affinity's search, which filters on field values, and the whole filter still runs locally on what comes back. A filter with no field condition is still refused (reading every company isn't practical: use--query). Conditions on Affinity's enriched fields are sent as they are, with a warning that Affinity's search can miss a few records' enriched values. The cursor carries the search (xsearch1:...).queryon companies and persons can filter on their fields (fields.<name>): Affinity's search applies what it can, the rest of thewhere(e.g.name contains) runs on what comes back. Awherewith no field condition is still refused (it would mean reading Affinity's whole directory).meta.serverFilterwith--include-meta; enriched-field conditions carry a warning that a few records' enriched values can be missed.- More of
querygoes to Affinity (list entries too):is_null/is_not_nullon every verified field type, multi-value fields included; multi-select dropdowns (eqmembership,in,has_any,has_all,contains/starts_wordon option text,!=);!=on numbers; andstarts_wordon a person or company field (person, multi-person and multi-company fields; one per query, at the top level of thewhere, plain text of 3+ letters). querysorts on Affinity's side when everyorderBykey is a sortable field (fields.<name>, up to 5, nogroupBy/aggregate): with alimit, the fetch stops once enough records pass the filter, instead of reading everything to sort it. This works on companies and persons without awheretoo.meta.serverFilter.sortsays so.- CLI:
team ls|get,user ls|get,meeting get,note attached,feedback send(asks for confirmation: the text leaves your organization). The MCP registry has them;feedback sendgoes through the user-confirmation flow.
Fixed¶
query --dry-runno longer claims a "server-side filter" for list entries that was never applied.
[2.0.1] - 2026-10-12¶
Changed (breaking)¶
Exception to semantic versioning: these renames break code written against 2.0.0, in a patch release. 2.0.0 had been out for under an hour, and the renames remove method names that said which Affinity API they used instead of what they do.
notes.list_v2()/iter_v2()→notes.feed()/iter_feed()(every note across the organization; the CLI'snote feed).notes.get_v2()→notes.get(), which now returnsNoteV2(includes=Trueadds the attached companies, persons and opportunities). The V1notes.get()(returningNote) is removed;notes.list()(one entity's notes, V1) is unchanged.files.list_v2()/iter_v2()→files.feed()/iter_feed()(every file you can see, newest first).files.get_v2()is removed.files.get_download_url()now asks V2 first: one request for the details and a signed URL (valid 60 seconds) instead of V1's two. It falls back to V1 when the client is pinned to an API version older than 2026-09-17, or when V2 refuses or doesn't find the file; the result (PresignedUrl) is the same either way.files.get()stays on V1 (EntityFile, with the file's person, company or opportunity) and works under any version pin.- CLI
note getreads the note from V2: same output keys,contentis the stored HTML (as innote create/updateoutput), and the attachment ids come from V2. - CLI
files readandfile-urlget the file's details and URL in one request where V2 can answer (V1's path otherwise).
Plugins¶
- SDK plugin: 2.0.0 → 2.0.1 — skill: the renamed note methods.
[2.0.0] - 2026-10-12¶
Highlights¶
SDK 2.0 moves to Affinity's V2 API wherever V2 can do the job. Webhooks, reminders, note
writes, list get/create, file reads (new files.get_v2/list_v2) and the rate-limit endpoint
now use V2; the CLI gains xaffinity webhook ls|get|create|update|delete. V1 stays only where
V2 can't do it, and for person create/delete, opportunity rename/delete and adding to a list,
which the V2 spec documents but the API didn't serve when tested. Breaking: webhook and
reminder models change shape, note writes return NoteV2, add_opportunity() is removed, and
operations added in newer Affinity API versions refuse a client pinned to an older one
(ApiVersionTooOldError). See the Migrating to 2.0
guide.
Changed (breaking)¶
- Webhooks use the V2 API (
/v2/webhooks, Affinity API 2026-07-15 or later, beta).WebhookSubscriptionis nowWebhook:webhook_url→url,disabled→status(WebhookStatus.ACTIVE/DISABLED),created_by→creator(PersonRef), pluscreated_at/updated_at.WebhookCreate(url=…, subscriptions=[…])requiressubscriptions([]= every event type).WebhookUpdatetakesurl,subscriptions,statusand sends only what is set.list()returns one page (ids, created/updated ranges,limit,cursor);iter()returns every webhook.update()returns the webhook as read back (two requests);delete()returnsNone. 1.x field names raiseValidationErrorinstead of being ignored. Without permission to manage all webhooks, the API lists, reads and changes only the caller's own webhooks. A client pinned to API version 2024-01-01 getsApiVersionTooOldError. - Reminders use the V2 API (
/v2/reminders;get/deleteneed API 2026-07-15,update2026-09-17).Reminderhas the V2 shape:creator,owner,completer,personarePersonRef(id only; look names up separately),company/opportunityareCompanyRef/OpportunityRef, recurrence isrecurrence(period_days,reset_type; the read-onlyreset_type/reminder_daysproperties still work), plusupdated_at.ReminderType,ReminderResetTypeandReminderStatusare string enums with the V2 values (member names unchanged; the 1.x integers are still accepted);ReminderResetType.MEETINGis now an alias ofEVENT.list()takeslimit(1-100),cursor,idsand created/updated ranges instead ofpage_size/page_token, and returns reminders soonest-due first.update()sends only the fields that are set (clear_content=Trueremoves the content) and returns the reminder as read back.list_batch()is on the sync client too, and fetches up to 50 entities per request. Kept on V1 where V2 can't do it: changing a reminder's type, and updating or deleting a reminder you didn't create and don't own (V2 refuses; the SDK retries through V1). A reminder must be attached to one person, company or opportunity (both APIs require it; the SDK now says so before sending). - Note writes use the V2 API (
notes.create,update,delete; reads are unchanged).create()andupdate()returnNoteV2(content.html,creator;update()reads the note back with its attachments, two requests). V2 stores notes as HTML: plain text (the defaulttype) is escaped and wrapped in paragraphs, andNoteUpdatetakestypetoo (plain text unless set toHTML, whatever the note was before). Withtype=NoteType.HTML,<b>/<i>become<strong>/<em>, and what the API rejects (style/class attributes, img, pre, code, blockquote, mention spans, non-http(s)/mailto links) raisesValueErrornaming the allowed tags. A reply (parent_id) with persons/companies/opportunities raises instead of ignoring them;AI_NOTETAKER/EMAIL_DERIVEDcan't be created.NoteUpdatecan replace a note's persons, companies and opportunities ([]clears). Changing the text of a note that @mentions people is refused by the API; the error now says so.NoteV2andNoteContentare exported fromaffinity.models. rate_limits.refresh()readsGET /v2/rate-limit(API 2026-07-15+), falling back to response headers as before (now also when the client is pinned to an older API version).RateLimitSnapshotis unchanged;RateLimitsalso reads the V2 names (callerPerMinute,orgPerMonth).- Lists:
get()andcreate()use the V2 API.get()no longer fillsfields(useget_fields()) oradditional_permissions; it setscreated_at.get_size()still reads the size from V1 (V2 has none).create()makes the caller the owner; a public list needs the "Share accessible Lists globally" permission. A list created withowner_idoradditional_permissionsstill goes through V1. entries(list_id).add_opportunity()is removed. Opportunities can't be added to a list as entries: create them on their list withopportunities.create(OpportunityCreate(list_id=...)).- Person create/delete, opportunity rename/delete and adding to a list stay on V1: the V2 spec documents them, but Affinity didn't serve them when tested (2026-10-12).
Added¶
files.get_v2(),files.list_v2(),files.iter_v2()(V2, Affinity API 2026-09-17+): a file's metadata with a download URL that expires after 60 seconds, and every file visible to you across the organization, newest first (filter by created date and uploader).FileV2model.files.get/list/iterstay on V1: only V1 lists one entity's files.- CLI
webhook ls|get|create|update|delete.createtakes--urland--event(repeatable) or--all-events;updatetakes--url,--event/--all-events,--status active|disabled.create,updateanddeleteask for confirmation (--yesin scripts), since a webhook sends CRM events to an outside URL. Rows includeallEvents. - CLI
list get <id>no longer prints.data.list.fields(the fields are in.data.fieldsas before),additionalPermissionsis always[], andcreatedAtis added;list create --owner-idcreates the list through V1. - CLI
reminder create/update --reminder-dayshelp now says what it is: the days between a recurring reminder's due dates. - CLI
note update --type plain-text|html;note create --typeis now plain-text or html (Affinity creates the other kinds itself).note create/updateoutput keeps thenote getkeys (typeishtml; content is the stored HTML). The duplicate-note warning still finds an earlier copy: V1 shows notes stored as HTML in a markdown form, which the check allows for. - CLI
reminder update --clear-content;reminder ls --page-sizeis now at most 100, and rows includeupdatedAt;--reset-type event(meetingstill accepted, printed asevent). - MCP: the webhook commands are in the command registry;
webhook create/update/deletego through the same user confirmation as deletes, and the confirmation says why (events sent to an outside URL).
Fixed¶
Person.company_idsandOpportunity.company_idsare now read from V1 responses (organization_ids), e.g. frompersons.create()with companies,persons.update(),opportunities.update()andget_details(); they were always empty.
Docs¶
- New API coverage reference page: every Affinity API operation (V2 and V1) and how the SDK supports it — the SDK call, an equivalent operation, or the planned release — with beta operations and the minimum API version marked.
Internal¶
tools/api_coverage.pyand the ledgertools/api_coverage.json(CI:--check): every operation in the pinned V2 snapshots and the new V1 snapshot (tools/api_coverage_v1_snapshot.json, 59 operations) has exactly one row.tests/test_api_coverage.pyruns each supported row's call, sync and async, against a recording HTTP layer and checks the method, path and API version it requests.
Plugins¶
- SDK plugin: 1.7.1 → 2.0.0 (matches SDK 2.0) — skill: note writes go through V2 (HTML, the
allowed tags), and
add_opportunity()is gone (create the opportunity on its list). - CLI plugin: 1.13.6 → 2.0.0 (matches CLI 2.0; no content change).
- MCP: 1.32.2 → 2.0.0 (requires CLI 2.0.0; see
mcp/CHANGELOG.md). - CLI plugin: 1.13.5 → 1.13.6 — skill: the Cowork install line adds
--no-warn-script-location(pip's "not on PATH" warning was noise; the skill adds the folder to PATH in each call), and the Cowork key paths (--env-file,AFFINITY_API_KEY_FILE) are quoted, so connected folders with spaces in their names work. - CLI plugin: 1.13.4 → 1.13.5 — no
cliscredential declaration. In local Cowork it routed everyxaffinitycall through Desktop's CLI credential broker, which isn't enabled yet and refuses the call ("cli-wrapper: plugin oauth disabled"), so the CLI never ran whatever key the VM had. Cowork's plugin settings no longer show an Affinity API Key field. The VM has noxaffinityof its own, so the skill now installs it when it's missing (pip install --user, about 10s, once per task) and puts~/.local/binon PATH in each call; the CLI then reads the key as the skill describes (a connected folder's.envwith--env-file, or a key file). - CLI plugin: 1.13.3 → 1.13.4 — in local Cowork the plugin's hooks run on the host Mac, not
in the VM where the CLI runs, so the key check saw the Mac's key (or none) and could deny every
data command even with a key set in Cowork. There the hooks now do nothing; a missing key
shows up as the CLI's own error. Cloud Cowork and Claude Code keep the check. The
.envread guard's message no longer claims the key is in the environment.
[1.29.0] - 2026-10-11¶
Highlights¶
field history-bulk works on global fields. A global field's changes (a company or person
field shown on a list) belong to the company or person, not to the list entry, so reading them
per entry returned nothing, with no warning. They are now read per company/person, or
field-wide when the field's history is small, and each row carries the company's entry on the
list.
Fixed¶
field history-bulkon a global field returned no rows. It now reads per company or person (one call each; entries sharing one are read once, with a warning) and fillslistEntryIdwith the entry on the list. With--allon 100+ of them it reads the field's org-wide history instead when that fits in one call; a forced--strategy fieldreads up to 200,000 changes and otherwise stops with an error. Rows include changes from before the company joined the list.field history-bulkwith a field that isn't on the list, or an enriched field, gave empty results; both are usage errors now. So is a global field with--list-entry-ids, which can't map entries to companies.field history --list-entry-idon a global field returned nothing; it now says the field is global and to use--company-id/--person-id.config check-key: when the key comes from a.envoutside the current directory (e.g.--env-filewith a Cowork connected folder, where each shell call starts above it), the suggestedpatternnames it with--env-file <path>, so later commands find it too.
Changed¶
field history-bulkreads field-wide 10,000 changes per call (was 500): fewer calls for list fields too. The dry run reportsfieldKind(list/global) and, for a global field,entities.
Plugins¶
- CLI plugin: 1.13.2 → 1.13.3 — skill:
history-bulkon global fields; Cowork keys in a connected folder need--env-file /sessions/<id>/mnt/<folder>/.env(each shell call starts in the session root), and the check-keypatterncarries it.
[1.28.0] - 2026-10-11¶
Highlights¶
Read the API key from 1Password Environments. AFFINITY_API_KEY_FILE (and --api-key-file)
accept a .env file and a named pipe, so an Environment's locally mounted .env file works directly:
the CLI waits for 1Password to supply the key after you approve the read, and the key never
touches the disk.
Added¶
AFFINITY_API_KEY_FILE/--api-key-file(SDKfrom_env()too): a.env-style file is read for itsAFFINITY_API_KEY=line (exportand quotes allowed); a file without that line is an error. A named pipe is read when its writer supplies the content, waiting up to 30 seconds, with a message naming 1Password if nothing arrives.config check-keyreports a named pipe as configured without reading it, and skips its API-version probe for it (reading would prompt). Tested with a real 1Password Environments locally mounted.envfile: approval and unlock prompts, repeated and concurrent reads.
Changed¶
field history-bulk --strategy fieldnever falls back to one call per entry: if the field-wide read (or reading the list's fields) fails, the command fails with a hint to retry or use--strategy entries.--strategy autostill falls back, with a warning.
Plugins¶
- CLI plugin: 1.12.0 → 1.13.0 — skill: field history without a selector,
field changes,history-bulk --strategy;interaction feed,note feed,note replies; transcripts;company|person relationships, merge history,task ls;field options. - CLI plugin: 1.13.0 → 1.13.1 — skill describes the current CLI only (no per-version notes); one upgrade hint at install.
- CLI plugin: 1.13.1 → 1.13.2 — skill:
AFFINITY_API_KEY_FILEreads.envfiles and 1Password Environments locally mounted.envfiles; forced--strategy fieldnever falls back. - SDK plugin: 1.7.0 → 1.7.1 — skill: API version pinning, note/file/semantic company search,
hidden values; new
references/services.mdorganised by question (field history, org-wide activity, notes V2 and replies, transcripts, relationships, merges, dropdown options, uploads).
[1.27.0] - 2026-10-10¶
Highlights¶
field history-bulk --all on a list field: a few calls instead of one per entry. For a field
that belongs to the list, on lists of 100 or more entries, the history is now read field-wide
(one call per 500 changes) and filtered to the list's entries: the same rows as before. A
9,000-entry pipeline no longer needs 9,000 calls.
Added¶
field history-bulk --strategy auto|field|entries(defaultauto). Dry run reportsstrategyand, forfield,estimatedApiCallsNote;estimatedApiCallsstays the per-entry worst case.
Changed¶
field history-bulkrows are sorted bychangedAt(they came in no particular order). Changes on entries no longer on the list are counted in a warning when read field-wide. If the field-wide read fails before any data arrives, it falls back to per-entry calls with a warning; a failure partway through is an error that suggests--strategy entries.
[1.26.0] - 2026-10-10¶
Highlights¶
Notes as V2, alongside V1. List notes across the organization with their creator, HTML
content, mentions, meeting and transcript links, reply counts and attached records
(note feed), and read a note's replies (note replies). note ls / note get and the V1
Note model are unchanged.
Added¶
- SDK
notes.list_v2()/iter_v2()/get_v2()(filters: creator, created and updated times;includes=Trueadds reply counts and attached company/person/opportunity previews) andnotes.list_replies()/iter_replies(), returningNoteV2. - SDK
persons/companies/opportunities:list_notes()/iter_notes()(V2). Company notes send Affinity API version 2026-07-15 (beta before); Affinity may refuse opportunity notes (AuthorizationError). NoteV2:replies_countandcompanies/persons/opportunities(+*_total), filled only withincludes;creatormay beNone.- CLI
note feedandnote replies.
[1.25.0] - 2026-10-10¶
Highlights¶
Org-wide activity, merge history and relationships. List emails, meetings, calls and chat
messages across the organization (interaction feed), past company and person merges
(merge-history, task ls), and who on your team knows people at a company, or a person, and
how well (company relationships, person relationships).
Added¶
- SDK
interactions.list_emails()/list_meetings()/list_calls()/list_chat_messages()anditer_*(): V2 org-wide lists withafter/before(sent or start time),created_afterandupdated_after. ModelsEmailV2,MeetingV2,CallV2,ChatMessageV2,InteractionAttendee(previews flattened into lists plus totals). Only items the key's user may see.interactions.list()(V1, one entity) is unchanged. - SDK
companies/persons:list_merges(),iter_merges(),get_merge_state()(modelsCompanyMergeState,PersonMergeState);tasks.list_merge_tasks("company"|"person")anditer_merge_tasks(). They need the "Manage duplicates" permission and the organization admin role. - SDK
companies/persons:list_relationships()/iter_relationships()(modelsRelationship,RelationshipPerson), withmin_score,orderandtotal_count. They send Affinity API version 2026-07-15 (beta before that); a client pinned to 2024-01-01 getsApiVersionTooOldError. - CLI
interaction feed --type email|meeting|call|chat-message,company|person merge-history ls|get,task ls --kind company-merge|person-merge,company|person relationships.
[1.24.0] - 2026-10-10¶
Not published separately: these changes ship in 1.25.0.
Highlights¶
Dropdown options and meeting transcripts. List a dropdown, ranked-dropdown or status
field's options and add, rename, recolor or delete them on list fields (field options); read
AI Notetaker transcripts with their dialogue (transcript ls, transcript get).
Added¶
- SDK
lists.get_field_dropdown_options(),companies.get_field_dropdown_options(),persons.get_field_dropdown_options()(every page).with_status_types=Truereads with API version 2026-07-15 so status fields reportstatus-dropdownwithstatus_category/win_rate(older versions show them asranked-dropdown). - SDK
lists.get_field_dropdown_option(),create_field_dropdown_option(),update_field_dropdown_option()(returns the option read back; the API answers 204) anddelete_field_dropdown_option(), checked like the API (types, required rank/color/status category, colors white/gray/blue/green/purple/orange/red, win rate 0-100). They send API version 2026-07-15. Deleting an option also clears it from every list entry that has it. DropdownOption:type,status_category,win_rate(V2;Nonefrom V1).- SDK
client.transcripts:list()/iter()(created-at window,total_count),get(),fragments()/iter_fragments(). ModelsTranscript,TranscriptFragment.PaginatedResponse.total_count(when the endpoint reports it).NoteV2gainsinteractionandtranscript_id. - CLI
field options ls|create|update|delete(deleteis destructive: confirmation names the option and the data loss;--yesin scripts) andtranscript ls|get. - HTTP client:
get(),get_all_pages(),get_url(),post()anddelete()acceptmin_api_version;get_all_pages()sends it on every page it follows.
Changed¶
- The CLI's dropdown lookups for field writes go through the new SDK methods (same requests, still without a version header, so clients pinned to 2024-01-01 keep working).
[1.23.0] - 2026-10-10¶
Highlights¶
Field history across all entities, and every change in the workspace. field history no
longer needs an entity: without one it returns a field's changes for every entity, bounded by
--changed-after and/or --max-results. The new field changes command (V2) lists changes
across all fields and entities, filtered by field, list entry, changer, time window and action:
the basis for delta sync and audits.
Added¶
- SDK
field_value_changes.list(): optional entity selector (at most one) and the new V1 parameterschanged_after(inclusive),limit,order_by("asc"/"desc") andafter_id, checked like the API checks them (after_idneedschanged_afterandorder_by="asc"). - SDK
field_value_changes.iter_all(): a field's whole history, oldest first, fetched in pages (keyset paging onchanged_atandid). - SDK
field_value_changes.list_global()/iter_global():GET /v2/field-value-changesacross all entities and fields, with filters for field(s), list entry(ies), changer, time window and action type (add/update/delete). New modelFieldValueChangeV2(withFieldValueChangeField,FieldValueChangeEntity,FieldValueChangeListEntry);valueis kept as the API sends it. - CLI
field changes(V2 org-wide list):--field-id/--list-entry-id(repeatable, OR),--changer-id,--changed-after,--changed-before,--action-type,--order, paging. - CLI
field history:--changed-afterand--order;--max-resultsis also sent to the API.
Changed¶
field history/field_value_changes.list()accept no entity selector (previously exactly one was required). The CLI then requires--changed-afteror--max-results.field_value_changes.iter()passes the new parameters through; its behaviour is otherwise unchanged (one request, the API's default order).--max-resultsonfield historywith--order desc(the default) returns the most recent N, as before; it is now also applied by the API.
[1.22.0] - 2026-10-10¶
Highlights¶
Merges ask for confirmation, like deletes. person merge and company merge remove the
duplicate record and cannot be undone; they are now marked destructive, prompt for confirmation
on a terminal, and take --yes to skip the prompt.
Breaking: scripts that run person merge / company merge must now confirm: add --yes
(or pipe in y); with no answer the command exits 2 (usage_error) and merges nothing. This is a stated
exception to the MAJOR-bump rule (VERSIONING.md): an irreversible command must not run
unconfirmed. If you use the MCP server, upgrade it to 1.25.1: MCP 1.25.0 cannot pass --yes
to merges, so merges fail (safely) with CLI 1.22.0. This includes the Claude Desktop extension:
update it to 1.25.1 or later together with the CLI.
Changed¶
- All delete commands and both merges share one confirmation path. The prompt is written to
stderr (stdout stays clean for
--json). A typed or pipedystill confirms andnstill aborts (exit 1). With no answer at all (end of input, e.g. a script without--yes) they now fail with a usage error (exit 2) in the normal JSON envelope instead of a bare exit 1, and never call the API. --help --json:person mergeandcompany mergehavedestructive: trueand a--yesparameter.
[1.21.0] - 2026-10-09¶
Highlights¶
Company and person field writes are all-or-nothing. company field and person field send
every --set, --set-json and --unset of a command in one request that Affinity applies
completely or not at all, like list entry field since 1.19.0. Enriched fields (including
"Current Organization") and Source of Introduction are written the same way.
Breaking: the JSON output of company field / person field changes: created items are
{fieldId, name, value}, cleared fields are listed in a new cleared, and deleted is gone.
--get returns V2 values (e.g. a dropdown as {dropdownOptionId, text}). These writes need
Affinity API version 2026-07-15 or newer: unpinned, the CLI sends it for the write; pinned to
2024-01-01, the command exits 2 (api_version_error) before sending anything.
EnrichedFieldNotWritableError is removed. This is a stated exception to the MAJOR-bump rule
(VERSIONING.md), decided for a clean single write path.
Also: list ls --query, faster list lookup by name, and calls that need a minimum API version.
Added¶
- Calls that need a minimum Affinity API version: an unpinned client sends that version for the
call; a client pinned to an older version raises
ApiVersionTooOldError(new, a subclass ofUnsupportedApiVersionError) before sending anything. SDKaffinity_api_versions_per_operation; CLImeta.affinityApiVersionPerOperation; such versions don't trigger the multi-version warning. InternalHTTPClient.patch(..., min_api_version=...). list ls --query TEXT: lists whose name contains the text (case-insensitive, matched by Affinity). SDK:term=onListService.list()/pages()(sync and async).- SDK:
CompanyService/PersonService.get_field_values(id, ids=...)(V2, keyed by field id) and.batch_update_fields(id, updates, value_types=...)(one all-or-nothing PATCH, at most 100 updates; sends API version 2026-07-15), sync and async.Affinity.require_api_version( minimum, operation=...).
Changed¶
- Breaking (CLI output):
company field/person fieldwrite every field of a command in one V2 request (all or nothing) instead of one V1 request per value; outputcreated({fieldId, name, value}) andcleared, nodeleted. A value equal to the current one is not rewritten (read through V2, so enriched fields are compared too).--getreads V2 values, so enriched fields return their value (they returnednullbefore). Writes need API version 2026-07-15+ (see Highlights). More than 100 fields in one command, or the same field twice (by name, other casing or id, or in both--setand--unset), exits 2 with nothing changed. - Hints for 401 / 403 errors (and
config setup-keyon a 403) name the documented causes: a mistyped or revoked key, the key's IP allowlist, in-app sharing and role permissions. - A single value for a multi-value text field (e.g.
--set Industry Fintech) is sent as a one-item list; numbers given for text fields (--set-json '{"Description": 42}') are sent as text (Affinity rejects both otherwise). ListService.resolve()/resolve_all()(and every CLI command that takes a list name) let Affinity narrow the lists by name first instead of reading every list; the exact, case-insensitive comparison is unchanged, and a miss still checks every list. Non-ASCII names always check every list.- MCP registry:
note search/file search/company searchreport their real limits (default 20 / 20 / 100, max 100);files readcommands no longer carry a limit config (their--limitis a byte size).
Removed¶
- Breaking (SDK):
EnrichedFieldNotWritableError— "Current Organization" and the other enriched fields are writable through V2, so nothing raises it any more.
Fixed¶
list entry field/opportunity field(and now company/person): setting a date with a time of day just after midnight UTC (e.g.2024-04-02T03:00:00Zwhen April 1 is stored) was skipped as "unchanged". Affinity keeps the UTC date of the value written (verified live), not the Pacific date of that instant.
Plugins¶
- SDK plugin: 1.6.1 → 1.7.0 — skill:
companies/persons.batch_update_fields()andget_field_values()(one all-or-nothing write, API version 2026-07-15),lists.pages(term=); "Current Organization" is writable (theEnrichedFieldNotWritableErrornote is gone). - CLI plugin: 1.11.0 → 1.12.0 — skill:
list ls --query;company field/person fieldwrites are all-or-nothing withcreated/clearedoutput and need API version 2026-07-15+; enriched fields (incl. Current Organization) are writable; stalefield updatereferences removed.
[1.20.0] - 2026-10-08¶
Highlights¶
Breaking: get_fields() no longer takes field_types=. Affinity's fields endpoints never
supported it, so it never filtered anything. To keep some field types, filter the result:
[f for f in client.companies.get_fields() if f.type == FieldType.GLOBAL].
Removed¶
field_types=fromget_fields()on companies, persons and lists (sync and async). The*/fieldsendpoints don't declarefieldTypesin any API version and Affinity dropped it silently, so every field was always returned (noted in 1.19.0). Passing it now raisesTypeError.field_types=on the data calls (companies.get/list/iter/all, the person equivalents, list entries) is unchanged. This removal ships in a minor release as a deliberate exception to the MAJOR-bump rule in VERSIONING.md, since the argument never worked.
[1.19.0] - 2026-10-08¶
Highlights¶
Field writes are all-or-nothing. list entry field and opportunity field send every
--set, --set-json and --unset of a command in one request that Affinity applies completely
or not at all: if it rejects one value, no field changes. Clearing a field no longer deletes
values one by one.
Search. note search, file search and company search (natural-language, ranked) search
the text of notes, the contents of files, and companies by description. They work under
--readonly.
All three Affinity API versions. Affinity's V2 API has versions 2024-01-01, 2026-07-15 and
2026-09-17; each API key has a default. Pin one with --api-version /
AFFINITY_API_VERSION / Affinity(affinity_api_version=...), see which one answered in
meta.affinityApiVersion, and see a key's default with whoami. Values Affinity hides on
restricted opportunities (2026-07-15+) are flagged instead of looking empty.
Also: uploads return the new file's id, new field value types, get_fields(filter=, includes=).
Added¶
- Pin the Affinity V2 API version:
Affinity(affinity_api_version="2026-09-17")/AsyncAffinity/ClientConfig, CLI--api-version, envAFFINITY_API_VERSION, profileapi_version(flag > env > profile). Accepts2024-01-01,2026-07-15,2026-09-17,current, orauto/key-defaultfor the API key's default (still the default behaviour). The header is sent on every V2 request includingnextUrlpages, never on V1, and is removed on cross-host redirects. Invalid values are rejected before any request; unknown dates warn. UnsupportedApiVersionErrorwhen Affinity rejects the pinned version (no silent fallback; CLI error typeapi_version_error, exit 2).client.affinity_api_versions_seenandclient.get_key_default_api_version();affinity.api_versionsmodule.- CLI JSON
meta.affinityApiVersion(the version that answered, or a list plus a warning if more than one did);whoamiandconfig check-keyreportkeyDefaultApiVersion. New guide "Affinity API versions". - Values Affinity hides (
type: "hidden", restricted opportunities, API 2026-07-15+):FieldValues.is_hidden()/hidden_fields(); the CLI warns that they are masked, not empty, wherever it warns about truncated values, and keeps them in "list-only" field output. xaffinity note search PROMPT,xaffinity file search PROMPT(newfilegroup) andxaffinity company search PROMPT.--company-idtakes the usual company selector;--note-id/--file-idlimit the search to given ids;company search --listfilters by company lists;--max-results/--limit/-nis 1–100 (larger values are clamped). JSONdatahas one row per hit;company searchputs Affinity's reading of the prompt inmeta.explanation.- SDK:
client.notes.search(),client.files.search(),client.companies.semantic_search()(sync and async) with modelsNoteSearchResult,FileSearchResult,SemanticSearchResult,SemanticCompanyand theNoteKindenum;HTTPClient.post(..., read_only=True)for read-only POSTs (allowed underWritePolicy.DENY, retried like GET). - SDK:
files.upload_returning_files(),upload_path_returning_files()andupload_bytes_returning_files()(sync and async) return the created files; they never raise after a successful upload. CLIcompany/person/opportunity files uploadrows includefileIdandcreatedAt. - SDK:
ListEntryService.batch_update_fields(..., value_types=)(sync and async) sends typed values, multi-value lists andNone(clears the field);BatchOperationResponse.operation. FieldValueType.FORMULA_NUMBER,LIST_MULTI,NOTE,REMINDERandFieldType.HIDDEN; table output renders note, reminder and formula-number values.get_fields(filter=, includes=)on companies, persons and lists (sync and async):filtername="X"(exact) orname=~X(substring), case-sensitive,|for OR;includesfilterability/sortability.ListSummary.creator_id,ListEntryWithEntity.creator_id.
Changed¶
list entry field/opportunity fieldoutput:createdlists{fieldId, name, value}per field set (the batch request returns no rows);clearedlists the fields--unsetcleared;deletedonly counts values removed by--unset-value.list entry field/opportunity field: field value types come from V2 metadata (V1's type 2 means "text or dropdown"); dropdown options are read fresh for every dropdown field written; the same field named twice (name, other casing or id) is refused; writes to types Affinity doesn't accept (interaction, formula-number, list-multi, note, reminder) are refused before any request; a timeout says the outcome is unknown and that re-running is safe; a 403 names restricted field access.opportunity field --set/--json/--unsetno longer writes through V1.- SDK response-cache keys and CLI session-cache keys include the configured API version. Existing session-cache entries are not reused after upgrading.
config check-keymakes one best-effort request (5 s, no retries) to report the key's default API version; its exit code is unchanged.field create --value-typeoffers only types Affinity can create (interaction,filterable-text,filterable-text-multiare no longer accepted); a-multitype implies--allows-multiple.is_publicon lists readspublicorisPublic; the company and person services rejectFieldType.HIDDENinfield_types=. The*/fieldsendpoints ignorefield_types(documented).- CLI JSON
metamay carryexplanation(fromcompany search). person get/company gettable output uses each list entry'slistNameinstead of fetching every list.
Deprecated¶
expected_v2_version: it never selected a version. Useaffinity_api_versionandaffinity_api_versions_seen.
Fixed¶
EntityFile.company_idwas alwaysNonefor files read through V1 (files.list(),files.get()).company ls/person ls --field-type listwas accepted; it now exits 2.field create --value-type <type>-multiwithout--allows-multiplecreated a single-value field.list entry field --appendon a field the CLI has no information about said it "holds one value"; it now says it can't tell and to use--set.- Upload docstrings described the wrong return value.
[1.18.1] - 2026-10-07¶
Highlights¶
Dropdown and status fields can be set from company field, person field and
opportunity field. Every --set of a dropdown on these commands failed with "Dropdown
option '…' not found", including opportunity field --set Status <stage>.
Fixed¶
- CLI
company field,person fieldandopportunity field --seton a dropdown, ranked dropdown or status field always exited 2 with "Dropdown option … not found": the field information they load (V2) carries no options. The options are now read fresh from Affinity for the dropdown fields being set (never from a cache). - Plain dropdowns on companies and persons are written by option id (V2), so a value can never create a new option on an account-wide field. Through V1, which these commands used, text that no longer matches an option (e.g. renamed moments earlier) silently creates one.
- Ranked dropdowns and status fields are written by option id; V1 rejects their text.
opportunity field --seton a field with no value failed with "list_entry_id is required".list entry field --seton a dropdown option added moments ago: the option is read fresh from Affinity before the value is refused.- Re-setting a dropdown to the option it already holds no longer rewrites it.
company/person/opportunity field --unsetwith a misspelled field name now fails before any--setin the same command is written.
[1.18.0] - 2026-10-07¶
Highlights¶
CLI field writes no longer lose data. CLI versions through 1.17.0 deleted a field's
values before writing the new ones, so any write Affinity rejected left the field empty.
Together with a second bug this hit everyday commands: list entry field --set <number field> 5
sent the number as text, Affinity rejected it, and the field ended up blank. Now:
list entry field --set replaces the value in one write (Affinity leaves the field untouched if
it rejects the write); company field, person field and opportunity field update a value in
place and add new values before removing old ones; numbers and locations are checked and sent
in the shape Affinity accepts; and --append on a multi-value location field adds instead of
replacing the list. Upgrade if anything writes fields through the CLI or the MCP server.
Breaking: --append on a field that holds a single value now exits with a usage error
instead of silently replacing the value; use --set.
Multi-value fields show when Affinity cut them off at 100 values (warnings in the CLI,
FieldValues.is_truncated() in the SDK), and opportunity field works again.
Added¶
- Truncation signal for multi-value fields. Affinity's V2 API returns at most 100 values per
multi-value field and reports the real number in
totalCount; nothing surfaced it, so a field with 250 values silently looked like it had 100. NewFieldValues.total_count(),is_truncated()andtruncated_fields(). The CLI warns ("Field 'X' on … shows 100 of 250 values") incompany get,person get,opportunity get,list entry get,list entry field --get,list export(JSONwarnings, stderr for CSV) andquery. query --output jsonnow includes awarningsarray when the run produced warnings. Execution warnings were previously dropped in every output format; other formats now print them to stderr.WebhookEvent.SMART_INTERACTION_VALUE_UPDATED(smart_interaction_value.updated), a webhook subscription type the V2 API now lists. Before, the event still parsed, but only as an unnamedUNKNOWN_…member.
Changed¶
- Breaking (CLI):
list entry field --appendon a field that holds a single value exits 2 ("use --set") before anything is written. It used to replace the value silently. - CLI
list entry field --seton a list entry no longer reportsdeletedin its JSON output: the old values are replaced by the write itself, not deleted first. - SDK: a
location/location-multivalue passed toupdate_field_value(..., value_type="location")(or"location-multi") is completed to the five keys Affinity requires (streetAddress,city,state,country,continent; missing ones sent asnull;street_addressis accepted). Affinity rejected a location missing any of them. ListEntryService.update_field_value()(sync and async) raisesValueErrorbefore sending more than 100 values for a company, person, number, text or location multi-value field, which Affinity rejects (dropdown-multi has no such limit).batch_update_fields()raisesValueErrorfor more than 100 updates (the API's limit per request) and for list values, which it used to send as an invalidtextvalue.
Fixed¶
- Data loss in CLI field writes.
list entry field --set/--set-jsondeleted every existing value of the field and then wrote the new one, so a write Affinity rejected (an invalid value, more than 100 values, a network error) left the field empty. It now sends a single write, which replaces the value or, if rejected, leaves it untouched.company field,person fieldandopportunity field(V1 writes) also deleted first; they now update a single value in place and, for multi-value fields, add the new values before removing the ones no longer wanted, so an interruption leaves extra values rather than none. - CLI
--seton a number field sent the number as text ("5"), which Affinity rejects; combined with the delete-first bug, it emptied the field. Numbers are now sent as numbers, and anything that isn't a number is refused before any write. - CLI location values (
--set HQ '{"city": "Paris"}') were sent as a raw string and always rejected. They are parsed as JSON objects, checked (unknown keys refused) and sent with all five keys Affinity requires. - CLI
list entry field --appendon a multi-value location (or legacy multi-value number) field would have replaced the whole list with the appended value; it now adds to it, and refuses before writing if the field would exceed 100 values. - CLI dropdown values written through V1 (
company/person/opportunity field --set) are sent as the exact text of the matched option. V1 silently creates a new option when given text that doesn't match one exactly. - CLI
opportunity fieldfailed with "list_id is required for opportunity field metadata" (exit 2) for every--set,--unsetand--get, since 0.5.0. It now loads the fields of the opportunity's list. ListEntryService.get_field_values()(sync and async) read only the first page of…/list-entries/{id}/fields(20 fields by default), silently missing the rest. It now reads every page.- CLI: warnings a command returned replaced the warnings collected while it ran instead of being merged with them.
Company.is_globalwas alwaysFalsefor companies read through the V2 API, which sendsisGlobal(the SDK read only V1'sglobal). Both keys are read now; JSON output still usesglobal.
[1.17.0] - 2026-10-07¶
Highlights¶
Company and person merges no longer need --beta (or
enable_beta_endpoints=True): Affinity made the merge endpoints generally available.
This also fixes merges through the MCP server, which always failed because it never
passes --beta. If you relied on BetaEndpointDisabledError to block merges, they
now run.
Date fields can be written reliably. --set <date field> 2024-04-01 used to fail
with HTTP 400, and the SDK's update_field_value() / batch_update_fields() raised
TypeError for a date or datetime. Both now land on the calendar day you gave,
and setting the date a field already holds is a no-op instead of a rewrite.
Fields created moments ago work in the CLI. list entry field --set and
list export --field no longer fail with "not found" while Affinity's V1 field listing
catches up with a new field.
Also: get_fields() returns every field in orgs with more than one page of them, the
CLI's proactive rate-limit throttle now actually fires, rate_limits.refresh() works on
plans without a monthly cap, and models keep the response fields Affinity added to V2
this year.
Added¶
- Models now keep response fields Affinity added to the V2 API in 2026 (previously parsed and
silently dropped):
Opportunity.list_name/is_restricted/is_redacted(a redacted opportunity'snameis masked as[Hidden]),AffinityList.created_at,ListSummary.created_at,ListEntry.list_name, andFieldMetadata.created_at/description/filterability/sortability.FieldMetadata.is_filterable/is_sortablereturnNonewhen the information was not requested, so "not requested" is never reported as "not filterable". All are optional with safe defaults (V1 payloads lack them). CLI JSON output gains these keys where the models are serialized (additive).
Changed¶
- Behavior change:
client.companies.merge()/client.persons.merge()(sync and async) and thecompany merge/person mergeCLI commands no longer requireenable_beta_endpoints=True/--beta. Affinity moved the merge endpoints from beta to GA in - Code that relied on
BetaEndpointDisabledErrorto stop merges from running will now run them.enable_beta_endpoints,--betaandBetaEndpointDisabledErrorremain for future beta endpoints; none currently requires them.
Fixed¶
- MCP
company merge/person mergealways failed withBetaEndpointDisabledError, because the MCP gateway never passes--betaand the flag cannot be set by config or environment. Fixed by the merge change above. - CLI: the proactive rate-limit throttle in
queryand the bulkfieldcommands never fired. It readX-RateLimit-Remaining, which Affinity does not send, from a lowercase-keyed dict; it now readsx-ratelimit-limit-user-remaining(any case) and pauses 0.5 s when fewer than 10 requests remain in the per-minute quota. The 429 backoff was unaffected. client.rate_limits.refresh()raised a validation error when the rate-limit response had no monthly org bucket, which happens on plans with no monthly cap (documented for v2; v1 and v2 share one request pool). The org bucket now comes back empty (all fieldsNone), matching the header-derived snapshot, andRateLimits.org_monthlyis optional.- CLI
--seton a date field with a plain date (e.g.2024-04-01) failed with HTTP 400 "value at/value/datadoes not match format: date-time": the value was sent as typed, and the V2 API requires a full date-time. Date-only input is now sent as noon UTC (the same calendar date in Pacific time, where Affinity stores it); a date-time is converted to UTC; unparseable input is rejected before any write. Found by a live write test. - CLI
list entry field --setandlist export --fieldon a list field created moments earlier could fail with "Field not found on list" / "Unknown field". Affinity's V1 field listing (which the CLI reads for dropdown options) can lag field creation by minutes, while V2 is current; the SDK and session caches could also pin a stale copy. On a miss the CLI now refetches once — V1 bypassing the cache (skip_cache, added in 1.11.0 but never used by the CLI) and refreshing the session cache — and adds list-specific fields that V2 already knows. A dropdown field found only via V2 has no options until V1 catches up. - CLI
field --setrewrote date fields on every run. Since 2026-01-01 Affinity stores date fields at midnight Pacific Time, so the exact-timestamp "already set" check never matched and the no-op short-circuit (1.15.0) never applied to dates. Setting the same calendar date is now a no-op: a date-only input is compared as the date typed, independent of the local timezone. TaskStatus.IN_PROGRESSandMergeStatus.IN_PROGRESSare now"in-progress", the spelling in the V2 spec ("in_progress"never appears there), so comparisons against a running task's status can match.MergeStatus("in_progress")still resolves for backward compatibility. Docs andtask statushelp updated.- List-entry field writes:
update_field_value()/batch_update_fields()now send adatetime/datevalue as typedatetimewith an ISO-8601 date-time (adateis sent as noon UTC, which is the same calendar date in Pacific time, where Affinity stores it), as their docstrings promised; they raisedTypeError: Object of type datetime is not JSON serializableinstead.AsyncListEntryService.update_field_value()sent the bare value without the V2{"type", "data"}wrapper; it now matches the sync method, including thevalue_typeparameter. Multi-value fields still need an explicitvalue_type. client.companies.get_fields(),client.persons.get_fields()andclient.lists.get_fields()(sync and async) read only the first page of the paged V2*/fieldsendpoints, so orgs with more fields than one page got silently truncated metadata (and field-name resolution in the CLI could fail). They now followpagination.nextUrl, and the merged result is what gets cached. NewHTTPClient.get_all_pages()/AsyncHTTPClient.get_all_pages().
[1.16.0] - 2026-10-07¶
Highlights¶
List field values are no longer reported as null on targeted lookups.
list export --company-id / --person-id, list entry get, and query
direct lookups (listId + id) now return the real Status, Owner, and other
list fields. Previously they returned every list field as null without a
warning, and agents doing a dedup check read those nulls as real empty data. If
you rely on these paths, upgrade.
You can now see what a plain company get / person get / opportunity get
did not fetch: meta.notRequested lists each skipped section with the flag
that fetches it (e.g. --expand list-entries). The CLI plugin (1.10.1) also
stops its pre-tool hook from blocking unrelated shell commands that merely
mention xaffinity, and its skill was reworked to read the right JSON keys.
Also in this release: packaging groundwork for set-once API-key entry in Claude Cowork. The CLI plugin
manifest now declares a clis.xaffinity block with an env.api_key credential
(AFFINITY_API_KEY, secret: true) and an api.affinity.co egress entry. This
targets Claude Desktop's host-side CLI-credential broker — the user enters the
key once under Customize → Plugins and it is injected into every Cowork
session that runs xaffinity, with no per-project .env needed.
Status: packaged but not yet user-visible. The desktop broker (UI field +
cowork-plugin-env encrypted store + invocation-time env injection) is
dark-launched behind an Anthropic server-side feature gate (2307090146,
internal name cli_plugin) that is off by default as of 2026-06-10. The
manifest is parsed and validated correctly by the current desktop build
(v1.11847.5) — verified against the binary — but the credential field will not
appear until Anthropic enables the gate for an account. No plugin update will
be required when it flips. Until then, end-user behavior is unchanged: provide
the key via AFFINITY_API_KEY, project .env + --dotenv, or
AFFINITY_API_KEY_FILE / _COMMAND (see 1.14.0).
Changed¶
- CLI plugin restructured to the standard Claude Code layout:
skills/andhooks/moved out of.claude-plugin/to the plugin root, with.claude-plugin/plugin.jsonnow carrying only the manifest. The marketplacesourceforaffinity-crm-cli-xaffinity-unofficialwas corrected to point at the plugin root (./plugins/xaffinity-cli) so Claude Code loads the real manifest rather than a stub. Hook test paths updated to match (tests/test_cli_{pre_xaffinity,session_setup}_hook.py).
Added¶
- JSON envelope:
meta.notRequestedoncompany get,person getandopportunity getlists data the cheap default lookup skipped, each with the flag that fetches it (e.g.{"key": "data.listEntries", "flag": "--expand list-entries"}). Absent when nothing was skipped, and an explicit--no-fieldsis not reported. Agents were readingfields: {"requested": false}and a missinglistEntrieskey as "no lists / empty fields".
Fixed¶
company get/person gethelp claimed--jsonreturns "Full data"; it returns no field values or list entries unless requested. Help now says so and points atdata.listEntries.list export --company-id/--person-idreturned every list field asnull. The targeted path fetched each entry with a bareGET /v2/lists/{id}/list-entries/{entryId}, which omits field values unlessfieldIds/fieldTypesis passed; it now requests the selected columns, the same as the streaming export path. Agents using this path for dedup checks were reading confident nulls over real data.list entry getlikewise showed no field values (fields.requested: false). It now requests the list's own fields (fieldTypes=list) and surfaces them inlistEntry.fieldswithmeta.resolved.fieldMetadata.queryonlistEntrieswithlistId+idequality (the direct-lookup fast path) returnedfields.*andentityNameasnull. It now requests the fields the query references and normalizes the record, matching the streaming path.- SDK:
ListEntryService.get()/AsyncListEntryService.get()accept keyword-onlyfield_idsandfield_types, matchinglist()/from_saved_view(). Default behavior is unchanged.
Plugins¶
- CLI plugin (
affinity-crm-cli-xaffinity-unofficial): 1.9.0 → 1.10.0 — addedclis.xaffinitycredential-broker manifest block (env.api_key→AFFINITY_API_KEY,network: ["api.affinity.co"]) for forward-compatible set-once key entry in Cowork (gated off server-side, see Highlights); plugin restructured to standard layout + marketplacesourcefix. - CLI plugin: 1.10.0 → 1.10.1 —
pre-xaffinity.shnow gates only actualxaffinityinvocations (command position, incl.cd x &&,$(...),VAR=v,uv run, a path to the binary). Previously any Bash command merely containing the text —grep/cat/git addofplugins/xaffinity-cli/...— was blocked when no key was configured. Skill reworked: - New "Read JSON from the right key" table and "null / missing does not mean
empty" rule, plus a section on reading one company's / person's lists and
list fields (
--expand list-entries,data.listEntries,meta.notRequested). - Fixed wrong examples:
person lsis.data.persons(was.data.rows, which silently yields anullowner ID);interaction ls/note ls/files lsreturn a bare.dataarray (was.data.interactions);primaryEmailAddress(wasprimaryEmail);list entry fieldtakes the list selector; pre-write validation shipped in CLI 1.15.0 (was "v0.7"). - SKILL.md cut from 24.1k to 14.1k characters (under the 19.9k compaction cap)
by moving setup, interactions, filtering, and files/write gotchas into
references/. Trigger text merged intodescription(Claude-onlywhen_to_useremoved). Regression prompts added undertests/skill-evals/xaffinity-cli-usage-evals/.
[1.15.0] - 2026-05-28¶
Highlights¶
Eliminates two retry hazards from a recent agent-driven intake incident: (1) the
four entity-field commands (entry/company/person/opportunity field --set)
now pre-validate every value up front — a bad person id or unknown dropdown
option aborts with exit 2 before any API write, instead of partial-committing
prior --sets. (2) Repeated --set of an unchanged value short-circuits both
DELETE and POST, so agent retries no longer pollute the audit log. Plus a
duplicate-note stderr warning on note create when an identical note from
the same author arrived within the last 5 minutes — informational only, the
create still proceeds.
Changed¶
- BREAKING (strict-by-default):
field --setnow performs upfront client-side validation across all four entity-field commands. Failures that the SDK can detect locally (invalid person/company id, unknown dropdown option, malformed datetime, etc.) raise a single aggregatedCLIErrorbefore any API write — no more partial commits when only some--setvalues are valid. Server-side failures (HTTP 400 mid-sequence) may still leave partial state because Affinity has no transactions. Seetest_entry_field_partial_failureintests/test_cli_entry_field.pyfor the server-side scenario. company/person/opportunity fieldcommands gain new client-side entity-reference validation. Previously--set Owner "<full name>"raised on the server after the prior--set Status=...had already committed; now it raises locally with zero writes. The CLI also surfaces a hint pointing atperson ls --query/company ls --query.--outputhelp text corrected: defaults totableregardless of pipe state. Pass--jsonexplicitly when piping to a JSON parser.
Added¶
- No-op short-circuit on
field --set: when the new value already matches the existing value (dropdown id, person id, number, datetime, text), skip both the DELETE and the POST. Multi-value--setfalls back to writing whenever the new set differs from the existing set (subset is NOT a no-op — set semantics is REPLACE). See per-type comparator rules inaffinity/cli/field_utils.py::value_equals_existing. note create --skip-duplicate-check/--duplicate-window-secondsflags. Default behavior emits a stderr warning when a content-identical note by the same author was created within the last 5 minutes; the create itself always proceeds. Useful safety net for AI agents that re-issue create after a parse failure on an earlier--json-less run.- Type-aware hint on
_coerce_entity_iderrors pointing atxaffinity person ls --json --query "<name>"orxaffinity company ls --json --query "<name>". - New helper module surface in
affinity.cli.field_utils:pre_validate_set_operations,value_equals_existing,execute_v1_set_phase,execute_v2_set_phase,execute_append_phase.
Fixed¶
- Opportunity field enriched-field writes (e.g.
--set "Source of Introduction" "...") now resolve to V1 numeric ids correctly. Previous code passed the enriched V2 literal directly toFieldValueCreate, breaking enriched-field writes on opportunities. Discovered while migrating to the shared helpers. entry field --appendno longer captures a stale existing-values snapshot between the set and append phases — the helper refresh handoff is explicit.
[1.14.0] - 2026-05-10¶
Highlights¶
Two new environment-variable conventions for resolving the Affinity API key from
external secret stores — AFFINITY_API_KEY_FILE (path to a file containing the key,
used by Docker secrets, k8s mounted Secrets, and Hashicorp Vault agent) and
AFFINITY_API_KEY_COMMAND (a shell command whose stdout is the key, used by
1Password CLI, pass, gpg, macOS Keychain, and git-credential-style helpers).
Both conventions are industry-standard 12-factor patterns and work with from_env(),
AsyncAffinity.from_env(), and the xaffinity CLI.
This release also bundles a previously-unreleased audit cleanup of the
xaffinity marketplace and plugin metadata (no behavior change for users on
existing install commands; brings on-disk state in line with Claude Code
v2.1.120 plugin/marketplace conventions).
Added¶
AFFINITY_API_KEY_FILE— set this env var to a file path; the SDK and CLI read the API key from that file at startup. Supports~expansion. File permissions are checked on Posix systems; aUserWarningis emitted if the file is group- or world-readable. Useful with Docker secrets (/run/secrets/…), k8s mounted Secrets, and Hashicorp Vault agent sidecar patterns.AFFINITY_API_KEY_COMMAND— set this env var to a shell command; the SDK and CLI run it at startup and use its stdout as the API key. Shell is used so that credential helper conventions (op read …,security find-generic-password -w,pass show …,vault kv get -field=…) work without quoting gymnastics. Default timeout is 30 s, overridable viaAFFINITY_API_KEY_COMMAND_TIMEOUT. Non-zero exit or empty stdout raises aConfigurationError/CLIError.- Both new env vars are tried between
AFFINITY_API_KEY(step 2) and the existing--api-key-file/--api-key-stdinCLI flags (steps 5–6) in the resolver chain. Empty string is treated as unset at every step. - New internal module
affinity/_internal/keyfile.pywithread_key_fileandread_key_commandhelpers shared between the SDK and CLI resolver call sites. xaffinity config check-keynow recognizesAFFINITY_API_KEY_FILEandAFFINITY_API_KEY_COMMANDas configured sources (reportssource: "file"/"command"in JSON output). Previously the check-key helper only knew aboutAFFINITY_API_KEYenv,.envfiles, and the profile config, so users who configured the key only via_FILEor_COMMANDgot a misleadingconfigured: false.- Marketplace (
xaffinity) metadata:owner.email,owner.url, and per-pluginhomepage,repository,license, andtagsfields (no new install commands; metadata only).
Changed¶
- CLI "missing API key" error message now enumerates all resolution paths (env vars, file/stdin flags, profile config) rather than naming a subset.
- Plugin manifest names aligned to marketplace entry names:
affinity-sdk→affinity-crm-sdk-unofficial,xaffinity-cli→affinity-crm-cli-xaffinity-unofficial. Existing install commands are unchanged for users on the published marketplace. - All
/plugin install|update|uninstall <short>@xaffinityreferences inREADME.md,docs/public/**,mcp/.claude-plugin/xaffinity-mcp.sh,mcp/README.md, and the GitHub release install footer updated to the correct long-form identifiers (affinity-crm-{sdk,cli-xaffinity,mcp}-unofficial@xaffinity). The previous short forms had never resolved against the published marketplace. - CLI plugin's
xaffinity-cli-usageskill: full resolution chain documented in theconfigured: falsebranch; Cowork-specific edge case now points users at project.env+--dotenvandAFFINITY_API_KEY_FILEas portable options. - MCP plugin's
affinity-mcp-workflowsskill: API-key-error troubleshooting row lists all five resolution paths. - Plugin skill frontmatter split into
description+when_to_use(SDK and CLI plugins).
Removed¶
- Dead
mcp/.claude-plugin/.mcp.json(inlinemcpServersinplugin.jsonis the source of truth).
Plugins¶
- SDK plugin (
affinity-crm-sdk-unofficial): 1.6.0 → 1.6.1 —plugin.json#namealignment + skill frontmatter split. - CLI plugin (
affinity-crm-cli-xaffinity-unofficial): 1.8.0 → 1.8.2 —plugin.json#namealignment + skill frontmatter split + newAFFINITY_API_KEY_FILE/_COMMANDdocumentation in the resolution-chain skill content. - MCP plugin (
affinity-crm-mcp-unofficial): 1.22.1 → 1.22.2 — troubleshooting skill row updated to list all key-resolution paths.
[1.13.1] - 2026-04-23¶
Highlights¶
Documentation corrections only — no behaviour changes. Fixes two places that cited "list-specific" as a valid fieldTypes value (it isn't; the correct V2 enum values are "enriched", "list", "global", and "relationship-intelligence").
Fixed¶
docs/public/cli-reference.md:fieldTypesexample corrected from"list-specific"to the complete valid set ("global","list","enriched","relationship-intelligence").affinity/models/entities.py:FieldMetadata.typeinline comment corrected from"list-specific"to"list"to match the actual V2 API enum value.CLAUDE.md: V2 Filter "silently ignored" section now correctly documents that companies, persons, and opportunities also rejectfilter=(previously only mentioned list entries).
[1.13.0] - 2026-04-22¶
Highlights¶
Breaking change heads-up: xaffinity list export --filter without a scope flag now exits 2. Add --all, --max-results N, or --first-page-only to pick the scope explicitly. The previous silent page-1-only result was the root cause of the "Fusion Mantle" false-duplicate incident.
Also new: entity-scoped --company-id/--person-id flags on list export for cheap, unambiguous duplicate checks; every JSON response now exposes meta.truncated and meta.truncationReason so automated callers can't miss an incomplete answer.
Added¶
- CLI:
list export --company-id ID/--person-id ID(both repeatable) for entity-scoped exports that map to the V2/companies/{id}/list-entries//persons/{id}/list-entriesendpoints. Primary use case: dedup-check beforelist add-entry— emits"Not on this list: company_ids=[...]"warning when a requested ID is absent. Currently scoped to company and person lists; opportunity-list support tracked separately. - CLI:
list export --first-page-onlyexplicit opt-in for first-page-only results (required alongside--filterif--all/--max-resultsare not set). - CLI/SDK:
meta.truncated(bool) andmeta.truncationReason(string) on allCommandResultenvelopes. Both keys are stripped from the JSON output whenNone. Known values fortruncationReason:firstPageOnly. - SDK:
_entry_to_filter_dictnow includeslistEntryId,entityId,entityName,entityType— filtering on the row-level keys the CLI outputs no longer silently matches zero rows.
Changed (breaking)¶
- CLI:
list export --filternow errors (exit code 2,usage_error) without one of--all,--max-results, or--first-page-only. Previous versions silently returned first-page-only results. - Migration: Add the intended scope flag. For "filter all list entries" →
--all; for "filter the first N" →--max-results N; for "filter what fits on page 1" →--first-page-only. On very large lists prefer--saved-view(server-side) or the new--company-id/--person-id(entity-scoped).
Fixed¶
- Agents redirecting stderr to
/dev/nullno longer silently lose the truncation signal — it's in the JSON envelope atmeta.truncated. list export --filter 'entityName =~ "..."'(and other row-level keys:entityId,entityType,listEntryId) no longer silently returns zero rows. These keys were previously absent from the filter dict.
Plugin / MCP updates (shipped alongside)¶
xaffinity-cliplugin: 1.7.0 → 1.8.0 — "Common pitfalls (READ THIS FIRST)" section added to thexaffinity-cli-usageskill covering--jsonvs NDJSON, stderr suppression, filter-scope requirement, dedup via--company-id, andmeta.truncated.xaffinity-mcpserver: 1.21.0 → 1.22.0 — minimum CLI bumped to 1.13.0 (mcp/COMPATIBILITY); data-model guide gains a "Duplicate checks" section with the--company-idworkflow. Seemcp/CHANGELOG.mdfor details.
[1.12.0] - 2026-04-20¶
Highlights¶
Two silent-failure classes eliminated at the SDK and CLI boundary: (1) filter= on companies.list() / persons.list() (and the query command against global entities) now raises instead of silently returning unfiltered data — server-side filtering is not supported on these list endpoints, and the previous behavior masked this; (2) companies.create() / persons.create() refuse to create a duplicate by default, raising DuplicateEntityError carrying the existing entity ID so callers can recover without producing duplicates.
Added¶
DuplicateEntityErrorexception (subclass ofAffinityError) exposingentity_type,existing_id,existing_name,existing_domain,existing_is_globalcompanies.create(data, *, if_not_exists=True)andpersons.create(data, *, if_not_exists=True)— sync and async. Defaults to pre-checking for duplicates via fuzzy search before creating- CLI
--allow-duplicateflag oncompany create/person createto bypass the pre-check - CLI
duplicate_existserror type (exit code 6) witherror.details.existing.{companyId|personId, name, domain, isGlobal}for recovery - CLI
unsupported_filtererror type (exit code 2) oncompany ls --filter,person ls --filter, andqueryon companies/persons/opportunities
Changed (breaking)¶
companies.list(),persons.list(), and their.get_first() / .all() / .iter() / .pages()variants now raiseValueErroriffilter=is passed. Previous versions silently returned unfiltered results because server-side filtering is unsupported on these list endpoints.queryCLI command:whereclauses oncompanies,persons,opportunitiesnow raiseQueryValidationErrorinstead of silently returning all records. The schema registry'sfilterable_fieldsis empty for these entities. UselistEntrieswith alistIdfilter for list-scoped queries.companies.create()/persons.create()signature adds keyword-onlyif_not_exists: bool = True. Existing callers that expected duplicate-creation must passif_not_exists=Falseexplicitly.- CLI
company create/person createexit with code 6 (duplicate_exists) instead of creating duplicates, unless--allow-duplicateis passed.
Fixed¶
- CLI
company ls/person lsno longer forward the--filterargument to the SDK when it isNone, avoiding false positives from the SDK-level rejection. queryCLI errors in--jsonmode now emit a structured error envelope on stdout (parseable by MCP and scripts) witherror.type,error.message,error.hint,error.details.
[1.11.0] - 2026-04-19¶
Highlights¶
xaffinity person field --set "Phone Number" "..." and similar enriched-field writes now work. The SDK and CLI accept the field name or its field ID and handle the rest — including disambiguating names that exist under multiple enrichment providers (e.g. company "Industry" exists for both the built-in enricher and Dealroom), which now raises AmbiguousFieldError with a candidate table instead of silently picking one. Truly derived fields like "Current Organization" raise the new EnrichedFieldNotWritableError instead of failing with an opaque message.
Added¶
EnrichedFieldNotWritableErrorexception (subclass ofUnsupportedOperationError) — raised when an enriched field is derived-only and cannot be writtenFieldService.list(skip_cache=True)/AsyncFieldService.list(skip_cache=True)— bypass the field-metadata cache for fresh post-write readsFieldResolver.to_v1_numeric(client, field_id, entity_type)on the CLI resolver — resolves field IDs to the numeric form the write path needs, handling enriched IDs transparently
Fixed¶
person field --set/person field --unset/company field --set/company field --unsetnow correctly handle enriched field IDs in addition to field names and numeric IDs- Ambiguous enriched field names (e.g. company "Industry") are now disambiguated by enrichment provider instead of silently picking one of the matching fields
[1.10.0] - 2026-04-05¶
Highlights¶
You can now resolve field names to FieldMetadata (including FieldId) using FieldResolver.find_field(), which is what you need for write operations like update_field_value(). Unlike get() which silently picks the first match for ambiguous names, find_field() raises AmbiguousFieldError to prevent accidentally updating the wrong field. Skill descriptions across all three plugins have been rewritten for clearer triggering boundaries when multiple plugins are installed.
Added¶
FieldResolver.find_field(name)method — resolves field names toFieldMetadatafor write operationsAmbiguousFieldErrorexception — raised whenfind_field()encounters ambiguous field names; includes candidate IDs and disambiguation hints
Changed¶
- Plugin skills: Clarified triggering boundaries across MCP, CLI, and SDK plugins — MCP workflows skill is now explicitly primary when MCP tools are available, CLI skill defers to MCP for general CRM tasks
- Plugin skills: Pipeline-history skill rewritten with MCP tool examples (was raw bash), query-language skill no longer depends on workflows-guide resource
- Plugin skills: SDK skill now documents
FieldResolver.find_field()patterns for read vs. write field resolution
[1.9.0] - 2026-03-22¶
Highlights¶
Accessing a company or person that was merged now raises a dedicated MergedEntityError (or CompanyMergedError/PersonMergedError) instead of a generic ValidationError. The exception provides structured source_id and target_id attributes, so you can programmatically follow merges without regex-parsing error messages. The CLI also surfaces this as a distinct entity_merged error type with structured JSON details.
Added¶
MergedEntityError,CompanyMergedError,PersonMergedErrorexception classes — raised on 422 responses indicating an entity was merged into another- CLI:
entity_mergederror type withsourceId,targetId,entityTypein JSON error details (exit code 4)
[1.8.2] - 2026-03-19¶
Highlights¶
Added a comparison guide documenting how this project differs from Affinity's official MCP Server (beta), covering API coverage, tool architecture, deployment options, and when to use each.
Added¶
- Comparison guide: Affinity SDK vs. Official MCP
- README section linking to Affinity's official MCP and the comparison guide
[1.8.1] - 2026-03-16¶
Highlights¶
InteractionDirection, InteractionLoggingType, and ListRole are now available from affinity.types — previously they could only be imported from the internal affinity.models.types path. The Interaction model now also captures the logging_type field from the API instead of silently dropping it.
Fixed¶
- Re-export
InteractionDirection,InteractionLoggingType, andListRolefromaffinity.types— these were the only public enums missing from the stable import path - Add
logging_typefield toInteractionmodel — the API returns this field but it was silently discarded
[1.8.0] - 2026-03-10¶
Highlights¶
entry field --get now returns resolved person and company objects (with firstName, lastName, primaryEmailAddress, etc.) instead of raw integer IDs. This matches the behavior of list export and provides a consistent, AI-agent-friendly experience. Under the hood, the command switched from V1 to V2 API.
CLI Plugin 1.6.1¶
Added¶
- Skill: documented file commands (
company files,person files,opportunity files) withls,download,read,uploadsubcommands — prevents unnecessary fallback to raw v1 API calls - Skill: added file commands to Quick Reference table
Changed¶
- Breaking:
entry field --getoutput for person/company reference fields now returns resolved objects instead of raw integer IDs. Scripts that parse the integer ID should update to read from the resolved object. entry field --getnow uses the V2 API (/v2/lists/{listId}/list-entries/{entryId}/fields) instead of V1field_values.list()
Fixed¶
ListEntryService.get_field_values()(sync and async) now correctly parses V2 API list response format — previously returned empty results due to dict/list type mismatch- Added
typestoREPEATABLE_QUERY_PARAMSso the V2 fields endpointtypesfilter parameter is correctly encoded FieldValues._coerce_from_apino longer double-wraps dicts that are already in{requested, data}format — fixesfields.*returning null after round-trip serialization- Query executor now warns (via
ctx.warnings) when field metadata fetch fails instead of silently returning null for allfields.*values - Query executor now warns when field name resolution fails in
whereclauses instead of silently skipping resolution - Multi-parent fetch path now normalizes list entry fields —
fields.*inselectworks correctly when query spans multiple parent lists
[1.7.3] - 2026-03-08¶
Highlights¶
UserId is now a subtype of PersonId, reflecting that workspace users are internal persons in Affinity's data model. Code that receives a UserId (e.g., Note.creator_id) can now pass it directly to any API accepting PersonId — no casting required.
Changed¶
UserIdnow extendsPersonIdinstead ofIntId—isinstance(UserId(1), PersonId)is nowTrue. This is an intentional runtime behavior change: workspace users are internal persons in Affinity's data model. Code that usedisinstanceto distinguish users from persons should useperson.type == PersonType.INTERNALinstead.- Simplified internal casts that previously converted
UserId→int→PersonId
Documentation¶
- data-model.md: clarified that user IDs and person IDs share the same ID space
[1.7.2] - 2026-03-03¶
Highlights¶
The xaffinity CLI plugin's SessionStart hook no longer runs pip install on every container start. Installation is deferred to first actual use via a self-installing wrapper, cutting session startup from ~30s to <1s in ephemeral environments like Cowork. Skill descriptions now display correctly in the Claude Code UI. Also documents two Affinity API limitations discovered via support: interaction entity association and enriched field constraints.
CLI Plugin 1.6.0¶
Added¶
- Lazy install: SessionStart hook now drops a lightweight self-installing wrapper instead of running
pip installunconditionally. The wrapper defers installation to first actual xaffinity use, with mkdir-based locking for concurrent safety, a marker file for install failure detection, and a 45s wait timeout for concurrent invocations. - PreToolUse hook: Install-failure detection via
$HOME/.xaffinity-install-statusmarker — blocks commands with a clear error when pip install failed, instead of cryptic failures. - PreToolUse hook: Lazy session cache start —
xaffinity session startis triggered on first xaffinity command instead of at session start, withCLAUDE_ENV_FILEpersistence.
Changed¶
- SessionStart timeout reduced from 60s to 10s (hook is now lightweight)
- PreToolUse Bash timeout increased from 10s to 60s (accommodates pip install via wrapper on first use)
- SessionStart status message updated to "Preparing Affinity CLI environment..."
Fixed¶
- Skill YAML frontmatter: replaced multi-line
>scalar with single-line description (Claude Code couldn't parse folded scalars, showing>instead of the description) - Skill: updated session cache documentation to reflect lazy initialization
Documentation¶
- Skill: documented that interactions cannot be associated with companies/opportunities via API (UI's "Also add to" feature has no API equivalent)
- Skill: documented that "Current Organization" is read-only via API and "Current Job Title" requires a separate
field updateafter person creation
SDK Plugin 1.5.5¶
Fixed¶
- Skill YAML frontmatter: replaced multi-line
>scalar with single-line description (same parsing fix as CLI plugin)
Documentation¶
- Data model: documented interaction entity association limitation and enriched field constraints (Current Organization, Current Job Title)
[1.7.1] - 2026-03-01¶
Highlights¶
Plugin skills now guide agents to consolidate multi-source CRM queries into single scripts instead of running separate commands that dump raw JSON into the conversation. The CLI plugin also auto-starts session caching at session begin, so metadata is shared across all commands without manual setup.
CLI Plugin 1.5.6¶
Added¶
- Skill: "Multi-Source Tasks" section guiding agents to write consolidated bash scripts (with session cache + jq) instead of running separate CLI commands that dump raw JSON into context
- Skill: "Extract only what you need" guidance with jq examples for single-command output filtering
- Skill: session cache fallback instruction for environments without automatic setup
- SessionStart hook: automatic session cache initialization (
xaffinity session start) withCLAUDE_ENV_FILEpersistence for Cowork
SDK Plugin 1.5.4¶
Added¶
- Skill: "Multi-Source Tasks: Output Only the Summary" section guiding agents to print concise summaries instead of raw
model_dump_json()output when combining multiple data sources
[1.7.0] - 2026-02-21¶
Highlights¶
New field history-bulk command fetches field change history across an entire list in one shot — useful for pipeline stage analysis, funnel conversion, and time-in-stage metrics. Also fixes --dotenv to search upward for .env files (matching standard dotenv behavior) and hardens the PreToolUse hook with a three-tier API key check.
Added¶
- CLI:
field history-bulkcommand for batch field change history across lists. Supports--list-id(with--all/--max-resultsbounding),--list-entry-idsfor specific entries,--action-typefiltering,--dry-runfor API cost estimation, and concurrent fetching viaXAFFINITY_CONCURRENCYenv var. Partial failures are reported as warnings without blocking successful entries. - CLI:
--listalias for--list-idonfield lsandfield history-bulkcommands (LLM-friendly shorthand). - MCP Plugin: Pipeline history analysis skill (
pipeline-history) with 5-step workflow: identify status field, export current state, dry-run estimate, fetch history, analyze transitions.
Fixed¶
- CLI:
--dotenvnow searches upward for.envfiles usingfind_dotenv(usecwd=True), matching standard python-dotenv behavior. Previously only checked the current working directory, which failed in Cowork VM sessions where the working directory differs from the.envlocation. - CLI Plugin: PreToolUse hook (
pre-xaffinity.sh) now uses three-tier API key detection: (1)AFFINITY_API_KEYenv var, (2)--dotenvcheck-key, (3) plain check-key (config.toml). Previously only checked the env var, blocking commands when the key was configured via.envor config file.
Documentation¶
- Added
list export --jsonoutput structure documentation to data model resource (fieldkey details,--field/--field-typerequirement). - Added
field history-bulkto MCP command registry for LLM discoverability. - Added field change history section to data model resource.
CLI Plugin 1.5.5¶
Fixed¶
- PreToolUse hook: three-tier API key detection (env var → dotenv → config.toml)
MCP Plugin 1.19.0¶
Added¶
- Pipeline history analysis skill for deal stage transition workflows
[1.6.2] - 2026-02-17¶
Highlights¶
Update notifications now work reliably. Previously, having multiple Python environments (e.g., dev virtualenv + system install) could silently poison the update cache, and the first config update-check run always said "never checked" instead of just checking.
Fixed¶
- CLI:
config update-checknow checks PyPI inline when cache is missing or stale, instead of showing "never checked" - CLI: Background update worker now receives version from spawning process, fixing version mismatch when multiple Python environments share a cache
- CLI: Smarter cache invalidation — upgrading no longer discards the cache; it adapts it
[1.6.1] - 2026-02-16¶
Highlights¶
config check-key --env-file <path> now correctly reads the specified file instead of only looking in the current directory. Fixes key discovery in Cowork VM sessions where the .env is on a mounted path.
Fixed¶
- CLI:
config check-keyignored--env-fileflag — always checkedCWD/.envinstead of the user-provided path
[1.6.0] - 2026-02-16¶
Highlights¶
New --include-me flag for interaction create auto-includes your person ID. Also fixes field writes for person/company multi-value fields -- entity IDs are now wrapped correctly for the V2 API, and --append properly merges instead of replacing.
Added¶
- CLI:
interaction create --include-meflag to auto-include current user's person ID via whoami - CLI: Enhanced validation error hint for interaction person_ids constraint (internal/external requirement)
Changed¶
- Added interaction create guidance to CLI plugin skill
- Added interaction create examples to MCP data model resource
Fixed¶
- CLI: Person/company field writes (
--set,--append) now correctly wrap entity IDs in{"id": <int>}for the V2 API. Previously sent raw integers, causingvalidation_error: value at /value/data is not null. - CLI:
--appendonperson-multi/company-multifields now merges with existing values instead of silently replacing them (same fix already existed fordropdown-multi). - CLI: Repeated
--appendon the same multi-value field (e.g.,--append Tags A --append Tags B) now aggregates into a single write, preventing the second write from overwriting the first. - CLI: V1 API
allows_multiplepromotion now applies topersonandcompanyfield types (previously onlydropdownwas promoted to its-multivariant). - MCP command registry: corrected
--participantsto--person-idforinteraction create
Plugin Releases — 2026-02-15¶
Plugin versions are now independent from the SDK version (see VERSIONING.md).
CLI Plugin 1.5.3¶
Changed¶
- Plugin versions are now independent from SDK version
Fixed¶
- Security: SessionStart hook no longer exports
AFFINITY_API_KEYto the environment. The key stays in.envand is read per-command via--dotenv, preventing the LLM from accessing it viaenvorecho $AFFINITY_API_KEY.
SDK Plugin 1.5.3¶
Changed¶
- Rewrote
affinity-python-sdkskill description following Anthropic skills guide formula
MCP Plugin 1.18.1¶
Changed¶
- Rewrote
query-languageSKILL.md (861→293 lines) with progressive disclosure; extracted detail to 4 reference files - Updated
affinity-mcp-workflowsskill description following guide formula
[1.5.2] - 2026-02-14¶
Highlights¶
Plugin improvements for Claude Code and Cowork: automatic environment setup via SessionStart hook, API key protection via .env read guard, and clearer plugin names in marketplace listings.
Added¶
- CLI Plugin: SessionStart hook (
session-setup.sh) for Cowork bootstrap — installs xaffinity, sets PATH, loads API key from.env - CLI Plugin: PreToolUse/Read guard (
guard-env-read.sh) blocks reading.envfiles to prevent API key exposure in conversation - CLI Plugin: Query command reference (
references/query-guide.md) - MCP Plugin: Query language reference files (
references/filter-operators.md,quantifiers.md,include-expand.md,output-formats.md)
Changed¶
- Marketplace: Renamed plugins from generic "sdk"/"cli"/"mcp" to "Affinity CRM SDK (unofficial)", "Affinity CRM CLI (xaffinity, unofficial)", "Affinity CRM MCP (unofficial)" for clarity in plugin lists.
- CLI Plugin: Rewrote skill description following Anthropic skills guide formula
- MCP Plugin: Rewrote
query-languageSKILL.md (861→293 lines) with progressive disclosure; extracted detail toreferences/ - MCP Plugin: Updated
affinity-mcp-workflowsskill description following guide formula - SDK Plugin: Updated
affinity-python-sdkskill description following guide formula
Removed¶
- CLI Plugin: Removed
/affinity-helpcommand (redundant — CLI skill auto-triggers on relevant prompts)
Fixed¶
- CLI Plugin:
.envparsing now handles quoted values and CRLF line endings - CLI Plugin: SessionStart hook writes to
CLAUDE_ENV_FILEare idempotent (no duplicate lines on re-run)
[1.5.1] - 2026-02-12¶
Highlights¶
Fixes writing to multi-select dropdown fields. --set and --append now correctly handle the array format required by the API, and --append properly merges with existing selections instead of replacing them.
Fixed¶
- CLI:
--setand--appendnow work correctly for dropdown-multi fields when V1 API returnsvalue_type="dropdown"withallows_multiple=True. Previously,resolve_dropdown_valueonly checkedvalue_type(notallows_multiple) to determine the payload format, sending{"dropdownOptionId": ID}instead of[{"dropdownOptionId": ID}]. - CLI:
--appendfor dropdown-multi fields now merges with existing values instead of replacing them. The V2 API replaces the entire option array on POST, so--appendnow reads existing selections, adds the new option (deduplicating), and sends the combined array.
[1.5.0] - 2026-02-11¶
Highlights¶
Interaction queries are now much more robust. Date ranges are validated up front, ranges over 1 year are auto-chunked seamlessly, and mixing naive/timezone-aware datetimes is caught with a clear error. Breaking: InteractionService.list() now requires start_time, end_time, and an entity ID -- callers already getting 422 errors will now get a clear SDK-level message instead.
Changed¶
- Breaking:
InteractionService.list()andAsyncInteractionService.list()now requirestart_time,end_time, and at least one entity ID (person_id,company_id, oropportunity_id). Previously these were optional, but the API always rejected calls without them (422). Callers that were passingNonefor these parameters were already getting API errors; this change surfaces the requirement at the SDK level with clear error messages. InteractionService.list()now validates date ranges:start_timemust be beforeend_time, and the range must not exceed 365 days.
Added¶
- SDK:
InteractionService.iter()andAsyncInteractionService.iter()now automatically chunk date ranges exceeding 365 days. Large ranges are split into <=365-day chunks with synthetic cursors bridging them, making iteration seamless. - SDK:
iter()defaultsend_timetodatetime.now(timezone.utc)when not provided, so callers only need to specifystart_time. - SDK: Timezone consistency validation — mixing naive and timezone-aware datetimes raises
ValueErrorwith guidance instead of a rawTypeError. - SDK:
_chunk_date_range()utility for splitting date ranges into API-compatible chunks. - CLI: Query executor now properly fetches interactions for
includeandexpandoperations. Previously, these paths calledinteractions.list()without requiredtype/date parameters and silently returned empty results.
Fixed¶
- SDK: Fixed falsy truthiness bugs in
InteractionService.list()wherePersonId(0),CompanyId(0),page_size=0, and emptypage_token=""were incorrectly dropped. Changedif x:toif x is not None:for all optional parameters.
[1.4.1] - 2026-02-11¶
Highlights¶
CLI errors from invalid commands or options now produce proper JSON error envelopes when --json is active, fixing broken downstream JSON parsers.
Fixed¶
- CLI: Click-level errors (unknown commands, invalid options) now emit a proper JSON error envelope (
{"ok": false, "error": {...}}) when--jsonor--output jsonis active. Previously, stdout was empty and only plain text went to stderr, breaking downstream JSON parsers. - CLI:
normalize_exception()now handlesclick.UsageError(→usage_error, exit code 2) andclick.ClickException(→error, exit code from exception) instead of misclassifying them asinternal_error.
[1.4.0] - 2026-02-10¶
Highlights¶
FieldResolver now resolves all value types to human-readable text -- not just dropdowns, but also persons, companies, locations, and interactions. Dropdown fields return rich DropdownOption objects with .text, .rank, and .color. Breaking: get_value() returns DropdownOption instead of raw IDs, and resolve_dropdowns parameter is replaced by ResolveMode.
Changed¶
- Breaking:
FieldValues.get_value()now returnsDropdownOptionobjects for dropdown/ranked-dropdown fields instead of rawintIDs. TheDropdownOptionhas.id,.text,.rank, and.colorattributes. This applies to all dropdown value types (dropdown,ranked-dropdown,dropdown-multi). - Breaking: Removed
FieldType.LIST_SPECIFIC. UseFieldType.LISTinstead. The V2 API uses"list"uniformly;"list-specific"was never a valid V2 value. - Breaking:
FieldResolver.get()parameterresolve_dropdowns: boolreplaced withresolve: ResolveMode(ResolveMode.RAWorResolveMode.TEXT).ResolveMode.TEXTresolves dropdowns, persons, companies, and locations to human-readable strings.
Added¶
- SDK:
ResolveModeenum (RAW,TEXT) for controllingFieldResolvervalue resolution. - SDK:
ResolveMode.TEXTnow resolves person, company, location, and interaction fields to human-readable strings (not just dropdowns). - SDK:
FieldResolversupports source-qualified field names ("dealroom:Description") for disambiguating enrichment fields with the same display name. Ambiguous bare names emit a one-time warning at access time. - SDK:
validate_entity_field_types()raisesValueErrorwhenFieldType.LISTis passed to company/person endpoints (which only acceptENRICHED,GLOBAL,RELATIONSHIP_INTELLIGENCE). - SDK:
DropdownOption.colornow accepts bothint(V1) andstr(V2) values.
Fixed¶
- SDK:
FieldValues._extract_value()now correctly recurses through{"data": {...}}envelopes for dropdown values, fixing ranked-dropdown and dropdown-multi extraction that previously returned the raw envelope dict. - SDK: Dropdown text resolution now works without V1 field metadata. Previously,
FieldResolverbuilt a lookup table fromFieldMetadata.dropdown_options(V1-only; always empty on V2). Now text is read directly from theDropdownOptionobject extracted from the field value. - SDK:
async_check_unreplied()replaced broken no-typeiter()call withasyncio.gather()per-type pattern for parallel interaction fetching. - CLI:
_extract_person_display_nameand_extract_person_namenow delegate to sharedresolve_person()utility.
Documentation¶
- Added
FieldResolverusage example to README "Working with Lists" section. - Added
FieldResolvermention to getting-started guide field gotchas, linking to performance guide. - Replaced raw field count with
FieldResolverusage inexamples/basic_usage.py. - Updated field-types-and-values guide: dropdown types now document
DropdownOptionreturn type. - Added "List Entry Field Access" section to performance guide explaining
entry.entity.fieldsdelegation.
[1.3.2] - 2026-02-08¶
Highlights¶
Fixes writing to dropdown-multi fields via --set and --append. Previously all dropdown-multi writes failed with a type mismatch error.
Fixed¶
- CLI:
--setand--appendnow work fordropdown-multifields. Previously,resolve_dropdown_value()only handleddropdownandranked-dropdown, causing all dropdown-multi writes to fail with "Field value type should be dropdown-multi" errors. The fix resolves option text/IDs and wraps them in the array format required by the V2 API.
[1.3.1] - 2026-02-06¶
Highlights¶
FieldResolver now works correctly with list entry objects -- it auto-delegates to the inner entity when fields were fetched on the entity rather than the list entry directly.
Fixed¶
- SDK:
FieldResolver.get()andget_by_id()now automatically delegate to the inner entity when called with aListEntryWithEntitywhose fields were not directly requested. Previously warned and returnedNoneeven whenentry.entity.fieldswere populated. - SDK:
FieldResolverduplicate field name warning now includes enrichment source (e.g., "dealroom" vs "affinity-data") instead of raw IDs, making enrichment provider collisions immediately clear.
[1.3.0] - 2026-02-06¶
Highlights¶
New list_batch() method on AsyncEntityFileService for fetching files across multiple entities concurrently with auto-pagination.
Added¶
- SDK:
list_batch()onAsyncEntityFileServicefor fetching files across multiple entities concurrently with auto-pagination. Acceptsperson_ids,company_ids, oropportunity_idswithmax_concurrentandon_error("raise"/"skip") parameters.
[1.2.0] - 2026-02-06¶
Highlights¶
New async batch methods for files and reminders: batch_get() on AsyncEntityFileService and list_batch() on AsyncReminderService for concurrent multi-entity operations. Also fixes falsy ID bugs across reminder and file services.
Added¶
- SDK:
batch_get()onAsyncEntityFileServicefor fetching file metadata concurrently with controlled concurrency. Supportsmax_concurrentandon_error("raise"/"skip") parameters. - SDK:
list_batch()onAsyncReminderServicefor fetching reminders across multiple entities concurrently with auto-pagination. Acceptsperson_ids,company_ids, oropportunity_idswith common filters.
Fixed¶
- SDK: Fixed truthiness bugs in
ReminderService.list(),AsyncReminderService.list(),EntityFileService.list(), andAsyncEntityFileService.list()wherePersonId(0),CompanyId(0), and other falsy values were silently dropped from query parameters. Changed allif <param>:guards toif <param> is not None:. - SDK: Fixed incorrect
ReminderStatusdocstrings that referenced non-existent "SNOOZED" and "COMPLETE" values. The actual enum values areCOMPLETED,ACTIVE, andOVERDUE.
[1.1.0] - 2026-02-05¶
Highlights¶
Major convenience release: FieldResolver for looking up field values by name instead of ID, batch_get() for concurrent entity fetching, read_only() factory for safe clients, and get_first() for quick single-entity lookups. Working with Affinity field data is now significantly easier.
Added¶
- SDK:
CompanyService.get_many(company_ids)convenience method for batch fetching multiple companies in a single API call. Alias forlist(ids=[...])with better discoverability. - SDK:
FieldValues.get(field_id)convenience method for safer access to field values by ID. Returns the field value dict orNoneif not found. - SDK:
Affinity.read_only()andAffinity.read_only_from_env()factory methods (and async variants) for creating clients that block all write operations. Useful for read-only scripts and dashboards. - SDK:
get_first()convenience method onCompanyService,PersonService,OpportunityService,ListService, andListEntryService(sync + async). Returns the first matching entity orNone. - SDK:
batch_get()onAsyncCompanyService,AsyncPersonService, andAsyncOpportunityServicefor fetching multiple entities with controlled concurrency. Supportsmax_concurrentandon_error("raise"/"skip") parameters. - SDK:
FieldValues.get_value(field_id)for extracting the unwrapped field value (e.g., returns"Active"instead of{"data": "Active"}). Handles text, dropdown, multi-value, and location fields. - SDK:
FieldResolverhelper class for looking up field values by name instead of ID. Supports case-insensitive matching, batch extraction viaget_many(), and dropdown option resolution. - Docs: "Field Lookup Patterns" section in performance guide documenting
FieldResolverusage, low-level access, and field metadata sources.
Fixed¶
- SDK:
FieldService.list()now caches results for 5 minutes (matching V2 field endpoints). Previously only V2 endpoints (companies.get_fields(),persons.get_fields()) were cached. - SDK:
FieldService.list(list_id=ListId(0))no longer skips thelist_idparameter. The falsy ID guard (if list_id:) was changed toif list_id is not None:.
[1.0.3] - 2026-02-03¶
Highlights¶
Fixes MCP timeout failures during large query include operations by emitting incremental progress every 10 records instead of only at start/end.
Fixed¶
- CLI: Query
includeoperations now emit incremental progress every 10 records during N+1 API calls. Previously, progress was only emitted at the start and end of include steps, causing MCP timeout to expire during long-running N+1 operations (e.g., 100 records withinclude: ["persons"]). The fix changes fromasyncio.gather()toasyncio.as_completed()for incremental progress emission.
[1.0.2] - 2026-02-03¶
Highlights¶
Fixes descending sort for string fields in queries (dates, names were silently returning ascending order) and corrects API call estimates for sorted queries that could cause premature timeouts.
Fixed¶
- CLI: Query
orderBywith descending direction now works correctly for string values (dates, names, etc.). Previously, descending sort on non-numeric fields silently returned results in ascending order because string values cannot be negated. The fix uses a comparison-inverting wrapper class for proper descending sort. - CLI: Query dry run now correctly estimates API calls when
orderByis present. Previously,estimatedApiCallsused thelimitvalue even though sorting requires fetching all records first. A query withlimit: 200andorderByon a 9000-record list showed 4 API calls instead of ~90. This caused dynamic timeout calculations to be too short.
[1.0.1] - 2026-02-01¶
Highlights¶
Fixes unreplied email/chat detection (--check-unreplied and expand: ["unreplied"]) which was silently failing, and adds required type parameter validation to InteractionService with clear error messages instead of cryptic 422s.
Fixed¶
- SDK:
InteractionService.list()anditer()now validate thattypeparameter is required, raisingValueErrorwith clear guidance instead of failing with cryptic API 422 errors. The Affinity V1 API has always required this parameter. - CLI:
--check-unrepliedandexpand: ["unreplied"]now work correctly. Previously, all unreplied checks silently failed (returning null) due to missingtypeparameter in interaction API calls. - CLI: Query
dryRunJSON output now includes a warning when estimated API calls exceed 50, helping LLMs notice expensive operations.
[1.0.0] - 2026-01-31¶
First stable release. The SDK and CLI APIs are now considered stable. Breaking changes will follow semantic versioning (major version bumps).
Added¶
- CLI: Auto-update notifications. The CLI now checks PyPI daily (in background) and displays a notification when a new version is available. Notifications are suppressed in non-interactive environments (
--quiet,--output json, CI, no TTY). Disable withXAFFINITY_NO_UPDATE_CHECK=1orupdate_check = falsein config. - CLI:
xaffinity config update-check --backgroundflag for non-blocking update checks. Used by MCP server to trigger background updates without waiting for results.
Fixed¶
- MCP: Add
AFFINITY_API_KEYto environment variable allowlist. API keys configured in Claude Desktop's MCP settings were being silently dropped, requiring manual config file creation as a workaround. - MCP: Detect CLI in macOS Python framework path (
/Library/Frameworks/Python.framework/Versions/*/bin/). Users who installed viapip installwith python.org Python were getting "xaffinity: command not found" errors.
[0.16.1] - 2026-01-27¶
Added¶
- Docs: "Verify Installation" section in CLI documentation with step-by-step health check instructions (
xaffinity --version,AFFINITY_API_KEY=... xaffinity whoami).
[0.16.0] - 2026-01-26¶
Changed (Breaking)¶
- SDK:
Person.interactionsandCompany.interactionsattribute type changed fromdict[str, Any]to typedInteractionsmodel. Migration: change dict access (person.interactions["last_event"]["person_ids"]) to attribute access (person.interactions.last_event.person_ids). TheInteractionsmodel provides typed access tofirst_email,last_email,first_event,last_event,next_event,last_chat_message, andlast_interactionfields, each containing anInteractionEventwithdateandperson_idsattributes.
Fixed¶
- SDK:
PersonService.get()field values are now properly validated asFieldValuemodels.
[0.15.1] - 2026-01-25¶
Fixed¶
- CLI:
query --output jsonnow supports cursor-based pagination when output is truncated. Previously, JSON truncation happened at the MCP layer after CLI execution, so no cursor was emitted. Now JSON truncation happens in the CLI layer (same as other formats), enabling proper resumption via--cursor.
[0.15.0] - 2026-01-23¶
Added¶
- CLI:
company files read,person files read,opportunity files readcommands to read file content with chunking support. Returns base64-encoded content with metadata (size,offset,length,hasMore,nextOffset). Use--offsetand--limitto fetch large files in chunks. Default chunk size is 1MB.
Fixed¶
- CLI:
--env-filenow implicitly enables dotenv loading when an explicit file path is provided (not the default.env). Previously, using--env-file .sandbox.envwithout--dotenvwould silently ignore the file. - CLI: Fixed dropdown field value resolution in
entry field --setand--appendcommands. The V2 API requires{"dropdownOptionId": ID}format, but the CLI was sending raw values. Now dropdown text (e.g., "In Progress") or numeric IDs are properly resolved to the V2 API format. - CLI: List field metadata now fetched from V1 API to include
dropdown_options, which V2 API omits.
0.14.0 - 2026-01-22¶
Added¶
- CLI:
company files ls,person files ls,opportunity files lscommands to list files attached to entities without downloading them. Supports pagination (--page-size,--cursor,--max-results,--all), selector resolution (ID, URL, name, domain, email), and MCP safety limits. - CLI:
file-urlcommand to get presigned download URLs for files. Returns URL valid for 60 seconds along with file metadata (name, size, contentType). Useful for programmatic access and MCP workflows. - CLI:
files download --file-idoption for single-file downloads. Downloads one file directly without creating a manifest. - SDK:
FilesService.get_download_url(file_id)method to get presigned download URLs without downloading content. ReturnsPresignedUrlwith URL, file metadata, and expiration info. - SDK:
PresignedUrldataclass exported fromaffinitypackage with fields:url,file_id,name,size,content_type,expires_in,expires_at.
Changed¶
- CLI:
files dumprenamed tofiles downloadfor clarity.
Fixed¶
- SDK:
CompanyService.get()andPersonService.get()now automatically fall back to V1 API when V2 returns 404. This handles V1→V2 eventual consistency issues where a search (company ls --query,person ls --query) finds an entity via V1, but a subsequentgetfails because V2 hasn't synced yet. The fallback is transparent and requires no code changes.
Known Issues¶
- MCP:
get-file-urltool returns valid presigned URLs, but Claude Desktop's WebFetch cannot accessuserfiles.affinity.codue to domain sandbox restrictions. This affects all Claude Desktop users - neither "Additional allowed domains" nor "All domains" settings work around this limitation (#19087, #11897). Workaround: Usefiles readcommand (returns content inline), copy URL to browser, or use CLI directly withfiles download --file-id.
0.13.1 - 2026-01-21¶
Fixed¶
- CLI: Query progress now emits
progress: 0inon_step_startevents, enabling mcp-bash timeout extension for slow first-page fetches
0.13.0 - 2026-01-21¶
Changed (Breaking)¶
- SDK:
AffinityList.list_sizefield removed. The V2 API returns incorrect values (often 0 for non-empty lists). Useclient.lists.get_size(list_id)instead, which fetches from the V1 API and caches for 5 minutes.
Added¶
- SDK:
ListService.get_size(list_id)andAsyncListService.get_size(list_id)methods to get accurate list size from the V1 API with automatic caching (5 minutes). Useforce=Trueto bypass cache when fresh data is critical.
0.12.0 - 2026-01-20¶
Added¶
- CLI:
query --cursoroption for resumable pagination when responses are truncated (exceed--max-output-bytes): - Streaming mode (simple queries): O(1) resumption via stored Affinity API cursor - no re-fetching of previous pages
- Full-fetch mode (queries with orderBy/aggregate): Results cached to disk, zero API calls on resume
- Cursor emitted to stderr as NDJSON
{"type": "cursor", "cursor": "...", "mode": "..."}for MCP extraction - CLI exits with code 100 when truncated (cursor available)
- Cache auto-cleanup on startup: LRU eviction (500MB limit), 1-hour TTL
- Validation: query hash, format mismatch, cache tampering all detected with clear errors
0.11.0 - 2026-01-20¶
Added¶
- CLI: Full scan protection when running via MCP gateway. Commands with pagination (
list export,person ls,company ls, etc.) now enforce limits: default 1000 records, max 10000 records. The--allflag is blocked with a clear error message guiding users to use explicit--max-resultsor cursor pagination instead.
Changed¶
- CLI:
--csvis now an alias for--output csv(consistent with--jsonbeing an alias for--output json) - CLI: CSV sub-options (
--csv-bom,--csv-header,--csv-mode) auto-enable CSV output when no format is specified - CLI: Error messages for output flag conflicts no longer have trailing periods (e.g.,
--csv and --json are mutually exclusiveinstead of ending with.)
Removed¶
- Breaking: CLI:
--prettyflag removed fromquerycommand (use| jq .for pretty JSON output)
Fixed¶
- CLI: Query engine now correctly computes
entityNamefor Person list entries. Previously,entityNamewasnullfor persons because the API returnsfirstName/lastNameinstead ofname. Now uses the same display name logic aslist export. - CLI: Table/CSV formatters now correctly detect Person entities with real API types (
external/internal) or no type field. Previously, formatters only checked fortype="person"which the API never actually returns, causing Person data to display as "object (N keys)" instead of "John Smith (id=123)".
Performance¶
- CLI: Session cache now caches person resolution (by email/name) and company resolution (by domain/name), reducing API calls when running multiple commands in a session pipeline.
- CLI: Person and company field resolution now uses session cache, avoiding redundant field definition fetches.
0.10.0 - 2026-01-19¶
Fixed¶
- SDK: Removed unsafe
asyncio.create_task()inAsyncAffinity.__del__that could cause tasks to be garbage collected before completion. Users must now use context managers or callclose()explicitly. - SDK: Added thread-safe locking to
SimpleCachefor concurrent access. - SDK: Fixed async client initialization race condition with asyncio lock.
- SDK: Fixed stream context manager cleanup on errors in HTTP client.
- SDK: Fixed empty/whitespace content handling in JSON response parsing.
- SDK: Added error handling for partial file cleanup on download failures.
- SDK: Improved filename sanitization for server-provided filenames (null bytes, control chars).
- CLI: Added validation for
--timeout,--max-columns,--max-records,--max-output-bytesto reject non-positive values. - CLI: Added validation for
--env-fileto check file exists when--dotenvis enabled. - CLI: Added file size limit (1MB) for config files to prevent memory exhaustion.
- CLI: Added file permission warnings for API key files (same as config files).
- CLI: Fixed TOML escaping to handle newlines, tabs, and carriage returns in API keys.
- CLI: Fixed JSON/TOML parse error handling with helpful error messages.
- CLI: Fixed type validation for list access in CSV/table output (verify first element is dict).
- CLI: Fixed exponential backoff calculation to cap exponent before computing power.
- CLI: Let
AuthenticationError/AuthorizationErrorpropagate in query executor instead of silent catch. - CLI: Added logging to silent exception handlers in entity validation.
Changed (Breaking)¶
- CLI:
--check-unreplied-emailsrenamed to--check-unrepliedwith support for both email and chat messages. - New
--unreplied-typesflag: comma-separated list of types to check (email,chat,all). Default:email,chat - Output now includes
typefield ("email" or "chat") in addition to date, daysSince, and subject - Chat messages have
nullsubject (no subject attribute) - Cross-type reply detection: email replied via chat (or vice versa) counts as "replied"
- CLI: Query
expand: ["unrepliedEmails"]renamed toexpand: ["unreplied"] - Same cross-type reply detection and multi-type support as CLI flag
- CSV columns renamed:
unrepliedEmailDate→unrepliedDate,unrepliedEmailDaysSince→unrepliedDaysSince,unrepliedEmailSubject→unrepliedSubject, plus newunrepliedTypecolumn
0.9.14 - 2026-01-19¶
Fixed¶
- CLI: Query
selectclause now automatically includesexpandfields. Previously, usingselectwithexpandwould filter out the expansion data (e.g.,interactionDates,unreplied), requiring users to explicitly list expansions inselect.
0.9.13 - 2026-01-19¶
Added¶
- CLI: Query
includeforlistEntriesnow supportspersons,companies,opportunities, andinteractions. Fetches related entities based on list entry entity type (e.g., company entries get associated persons). - CLI: Query
expand: ["unrepliedEmails"]now works withlistEntries. Checks each list entry's underlying entity for unreplied incoming emails. - CLI: Query extended include syntax with parameters:
{include: {interactions: {limit: 50, days: 180}}}- Limit and lookback control{include: {opportunities: {list: "Pipeline"}}}- Scope to specific opportunity list{include: {persons: {where: {...}}}}- Filter included entities
Fixed¶
- CLI: Query TOON format now correctly flattens
fields.*andinteractionDateslike markdown and CSV formats do. Previously, TOON was missing the_apply_explicit_flattening()call, causing nested fields to be truncated and interaction dates to be missing entirely. - CLI:
list export --check-unreplied-emailsnow works standalone without requiring--expand. - CLI:
expand: ["interactionDates"]now always produces 8 canonical columns regardless of data presence, ensuring consistent schema across all records. - SDK: Improved error message when adding wrong entity type to a list (e.g., adding a company to a person list). Now provides clear guidance about list type requirements instead of exposing raw API validation error.
Performance¶
- CLI: Query
expand: ["interactionDates"]is significantly faster with parallel person resolution, bounded concurrent fetches (semaphore=10), and parallelized section resolution. Default concurrency increased from 5 to 15 (tunable viaXAFFINITY_QUERY_CONCURRENCY).
0.9.12 - 2026-01-17¶
Changed¶
- CLI: Query
listEntriesnow normalizes reference field values to display strings: - Person fields:
{"firstName": "Jane", "lastName": "Doe"}→"Jane Doe" - Company fields:
{"name": "Acme Corp", "id": 123}→"Acme Corp" - Multi-person/company fields: Arrays of names instead of raw objects
- Use
expandorincludefor full entity data when needed - CLI: Query output with explicit
selectonfields.*paths now flattens fields to top-level columns in table/CSV/markdown formats. JSON output preserves nested structure.
0.9.11 - 2026-01-17¶
Added¶
- CLI: Query
includeclause now displays included relationships inline by default. Use--include-style=separatefor separate tables or--include-style=ids-onlyfor raw IDs. JSON output includes fullincludedandincluded_by_parentmappings for correlation. - CLI: Query
includeextended syntax for custom display fields:include: {companies: {display: ["name", "domain"]}}. - SDK:
with_interaction_datesandwith_interaction_personsparameters forCompanyService.get()andPersonService.get(). When enabled, routes to V1 API to fetch interaction date summaries (last/next meeting, email dates, team member IDs). - CLI:
--expand interactionsoption forlist exportcommand. Adds interaction date summaries to each list entry (last meeting, next meeting, last email, last interaction with daysSince/daysUntil calculations and team member names). Supports both JSON and CSV output formats. - CLI:
expand: ["interactionDates"]support inquerycommand. Enriches records with interaction date summaries directly on each record inresult.data. Works withpersons,companies, andlistEntriesqueries. - CLI:
--check-unreplied-emailsflag forlist exportcommand. Detects unreplied incoming emails for each list entry and adds date, daysSince, and subject to output. Use--unreplied-lookback-daysto configure lookback period (default: 30 days). - CLI:
--with-interaction-datesand--with-interaction-personsflags forcompany getandperson getcommands. Fetches interaction date summaries directly in entity get operations. - MCP: Query tool
formatparameter now functional (was previously ignored). Supportstoon,markdown,json,jsonl,csv. - CLI:
--max-output-bytesoption forquerycommand. Enables format-aware truncation for MCP use, returning exit code 100 when truncated. - CLI:
format_toon_envelope()function for TOON output with full envelope (data[N]{...}:,pagination:,included_*:sections). - SDK: Batch association methods for PersonService, CompanyService, and OpportunityService:
get_associated_company_ids_batch(person_ids)/get_associated_opportunity_ids_batch(person_ids)get_associated_person_ids_batch(company_ids)/get_associated_opportunity_ids_batch(company_ids)get_associated_person_ids_batch(opportunity_ids)/get_associated_company_ids_batch(opportunity_ids)- All return
dict[EntityId, list[AssocId]]withon_error="raise"|"skip"parameter. - SDK:
retriesparameter onpersons.get(),companies.get(), andopportunities.get()methods. Enables automatic retry with exponential backoff on 404 errors to handle V1→V2 eventual consistency after create operations. Default isretries=0(fail fast). - CLI: Reminder date options now accept relative dates and keywords in addition to ISO-8601:
--due-date:+7d,+2w,+1m,+1y,today,tomorrow,yesterday,now--due-after,--due-before: Same formats for filtering inreminder ls- Example:
xaffinity reminder create --due-date +7d --type one-time --owner-id 123 - CLI: Domain validation for
company createandcompany updatenow provides helpful error messages: - Detects underscores (RFC 1035 violation) and suggests dash replacement
- Detects URL prefixes and extracts domain
- Example:
--domain test_company.com→ "Use 'test-company.com' instead" - Docs: V1→V2 eventual consistency guide covering 404 after create and stale data after update scenarios.
- Tests: Integration test suite for SDK write operations (
tests/integration/).
Changed¶
- SDK:
OpportunityService.get_associated_people()andget_associated_companies()now use V2 batch lookup instead of individual V1 fetches, reducing N+1 API calls (e.g., 50 people now fetched in 2 calls instead of 51). - SDK: Query executor
_batch_fetch_by_ids()now uses V2 batch lookup for persons and companies, improving query performance on relationship includes. - CLI: Query
includeclause now fetches relationship IDs in parallel, then batch-fetches full records via V2 API, reducing API calls from N×M to N+1 for deduped lookups.
Changed (Breaking)¶
- MCP: Query tool default format changed from
jsontotoonfor better token efficiency (~40% fewer tokens). - CLI: TOON query output now includes full envelope structure instead of data-only format. The
dataprefix is added to the array header:data[N]{fields}:instead of[N]{fields}:.
Fixed¶
- MCP: Query tool now honors the
formatparameter instead of always using JSON output. - SDK:
ListEntryService.batch_update_fields()now uses correct V2 API payload format. Previously failed with "Missing discriminator for property operation" error.
0.9.9 - 2026-01-14¶
Added¶
- CLI:
querycommand now supports advanced relationship filtering: allquantifier: Filter where all related items match a condition (e.g., find persons where all their companies have ".com" domains)nonequantifier: Filter where no related items match a condition (e.g., find persons with no spam interactions)existssubquery: Filter where at least one related item exists, optionally matching a condition (e.g., find persons who have email interactions)_countpseudo-field: Filter by count of related items (e.g.,"path": "companies._count", "op": "gte", "value": 2)- Available relationship paths: persons→companies/opportunities/interactions/notes/listEntries, companies→persons/opportunities/interactions/notes/listEntries, opportunities→persons/companies/interactions
- Note: These features cause N+1 API calls to fetch relationship data; use
--dry-runto preview
Changed (Breaking)¶
- CLI: Renamed relationship
"people"to"persons"for consistency with entity type names: - Query
include:{"from": "companies", "include": ["people"]}→ use["persons"] - CLI
--expand:xaffinity company get <id> --expand people→ use--expand persons - JSON output:
data.people→data.persons
Fixed¶
- CLI: Query engine no longer silently passes all records for
all,none, and_countfilters. Previously these were placeholder implementations that returnedTruefor all records, causing incorrect query results. (Bug #15)
0.9.8 - 2026-01-12¶
Fixed¶
- CLI:
querycommand now correctly fetches all records before applying filter, sort, or aggregate operations. Previously, limits were applied during fetch which caused incorrect results: - With filters: Empty results when matching records were beyond the limit position
- With sort + limit: Random N records sorted instead of actual top N
- With aggregate: Inaccurate counts/sums computed on partial data
0.9.7 - 2026-01-12¶
Fixed¶
- CI: Smoke test now correctly installs CLI extras before testing CLI import
0.9.6 - 2026-01-12¶
Added¶
- CLI:
listEntriesqueries now include convenience aliases:listEntryId,entityId,entityName,entityType. These intuitive field names work in bothselectandwhereclauses. - CLI:
listEntriesrecords now always include afieldskey (defaults to{}if no custom fields).
Changed¶
- CLI: Query projection now includes null values for explicitly selected fields. Previously,
select: ["entityName", "fields.Status"]would return{}if Status was null; now returns{"entityName": "Acme", "fields": {"Status": null}}.
0.9.5 - 2026-01-12¶
Added¶
- CLI: New
--output/-ooption supporting multiple formats:json,jsonl,markdown,toon,csv,table(default). markdown: GitHub-flavored markdown tables, best for LLM analysis and comprehensiontoon: Token-Optimized Object Notation, 30-60% fewer tokens than JSON for large datasetsjsonl: JSON Lines format, one object per line for streaming workflows- Example:
xaffinity person ls --output markdown,xaffinity query -o toon - Existing
--csvand--jsonflags continue to work as before.
Changed¶
- CLI:
to_cell()now extracts "text" from dropdown/multi-select fields instead of JSON-serializing the full dict. This makes CSV and other tabular outputs human-readable for dropdown values.
Fixed¶
- CLI:
querycommand withlimitnow correctly returns results when combined with client-side filters (likehas_anyon multi-select fields). Previously, the limit was applied during fetch before filtering, causing empty results when the first N records didn't match the filter criteria.
0.9.4 - 2026-01-12¶
Added¶
- CLI:
querycommand now supportshas_anyandhas_alloperators for multi-select field filtering. - SDK/CLI: Filter parser now supports V2 API comparison operators:
>,>=,<,<=for numeric/date comparisons. - SDK/CLI: Filter parser now supports word-based operator aliases for LLM/human clarity:
contains,starts_with,ends_with(string matching)gt,gte,lt,lte(numeric/date comparisons)is null,is not null,is empty(null/empty checks)- SDK/CLI: Filter parser now supports collection bracket syntax
[A, B, C]with operators: in [A, B]- value is one of the listed valuesbetween [1, 10]- value is in range (inclusive)has_any [A, B]- array field contains any of the valueshas_all [A, B]- array field contains all of the valuescontains_any [A, B]- substring match for any termcontains_all [A, B]- substring match for all terms= [A, B]- set equality (array has exactly these elements)=~ [A, B]- V2 API collection contains (array contains all elements)
Fixed¶
- CLI:
querycommand now correctly filters on multi-select dropdown fields (like "Team Member"). Theeqoperator checks array membership for scalar values and set equality for array values. Previously, these queries returned 0 results due to strict equality comparison. - SDK/CLI:
list export --filternow correctly matches multi-select dropdown fields. The=,!=, and=~operators now handle array values properly. Also fixes extraction of text values from multi-select dropdown API responses. - SDK/CLI: Fixed
=^(starts_with) and=$(ends_with) operators which were broken due to tokenizer ordering issue.
Improved¶
- SDK/CLI: Filter parser now provides helpful hints for common mistakes:
- Multi-word field names: suggests quoting (
"Team Member") - Multi-word values: suggests quoting (
"Intro Meeting") - SQL keywords (
AND,OR): suggests correct symbols (&,|) - Double equals (
==): suggests single=
0.9.3 - 2026-01-11¶
Changed¶
- CI: SDK releases now include MCPB bundle and plugin ZIP for convenience.
- CI: Enabled PyPI attestations via workflow_dispatch API trigger.
0.9.2 - 2026-01-11¶
Fixed¶
- SDK:
AsyncListEntryService.pages()now supportsprogress_callbackparameter (sync/async parity fix).
Changed¶
- BREAKING: CLI:
interaction lsJSON output restructured for consistency: .data.interactions→.data(direct array).data.metadata.totalRows→.meta.summary.totalRows.data.metadata.dateRange→.meta.summary.dateRange.data.metadata.typeStats→.meta.summary.typeBreakdown- BREAKING: CLI:
note lsJSON output restructured for consistency: .data.notes→.data(direct array)- Pagination:
.data.notes.nextCursor→.meta.pagination.nextCursor - BREAKING: CLI:
queryJSON output (with--include-meta) restructured for consistency: .meta.recordCount→.meta.summary.totalRows- Included entity counts now in
.meta.summary.includedCounts - CLI: Standardized
ResultSummaryfooter rendering across all commands (displays row counts, date ranges, type breakdowns as compact footer text instead of tables).
0.9.1 - 2026-01-11¶
Added¶
- CLI:
querycommand now validates entity queryability and provides clear error messages for unsupported entities. - CLI:
querycommand resolves field names to IDs automatically (e.g.,"field": "Status"works alongside"fieldId": 123).
Fixed¶
- CLI:
queryforlistEntriesentity now correctly requireslistIdfilter. - CLI:
queryrelationship definitions now correctly setrequires_n_plus_1flag for proper query planning.
0.9.0 - 2026-01-11¶
Added¶
- CLI: New
querycommand for structured JSON queries with complex filtering, includes, aggregations, and sorting. Use--dry-runto preview execution plans. Supports entities: persons, companies, opportunities, listEntries, interactions, notes. - MCP: New
querytool for complex data queries via JSON query language. Supports filtering (AND/OR/NOT), includes (related entities), aggregations (count/sum/avg/min/max), groupBy, and sorting. - CLI:
--limitalias for--max-resultsoncompany get,person get, andopportunity getcommands (consistency withlscommands).
Changed¶
- CLI:
--list,--list-entry-field, and--show-list-entry-fieldsnow auto-imply--expand list-entriesoncompany getandperson getcommands (improved DX).
0.8.6 - 2026-01-10¶
Added¶
- SDK:
PersonService.get_associated_company_ids()andget_associated_opportunity_ids()methods for symmetric association API. - SDK:
CompanyService.get_associated_opportunity_ids()method.
0.8.5 - 2026-01-10¶
Fixed¶
- CLI/SDK:
FilterParseErrornow raised when filter expressions fail to parse (previously silently ignored). Common cause: unquoted multi-word values like--filter 'Status=Intro Meeting'must be quoted:--filter 'Status="Intro Meeting"'. - CLI: Pre-commit hook now validates installed CLI version matches
pyproject.tomlbefore regenerating MCP registry.
0.8.4 - 2026-01-10¶
Added¶
- CLI: NDJSON progress output for
interaction lsmulti-type queries (MCP integration).
0.8.3 - 2026-01-10¶
Added¶
- CLI:
--limitalias for--max-resultson alllscommands (LLM-friendly). - CLI: Option aliases now included in
--help --jsonoutput.
0.8.2 - 2026-01-10¶
No user-facing changes. Version bump for PyPI release.
0.8.1 - 2026-01-10¶
No user-facing changes. Version bump for PyPI release.
0.8.0 - 2026-01-10¶
Changed¶
- CLI: Renamed
--jsonto--set-jsononperson field,company field,opportunity fieldcommands to avoid conflict with global--jsonoutput flag. - BREAKING: CLI:
--csv FILEis now--csvflag that outputs CSV to stdout. Use shell redirection:--csv > file.csv. Applies toperson ls,company ls,opportunity ls,list export. - CLI:
--csvand--jsonare now mutually exclusive (error if both specified). - CLI:
person ls --queryandcompany ls --querynow support--fieldand--field-typeoptions via hybrid V1→V2 fetch. - BREAKING: CLI:
interaction lsdate filter parameters renamed for consistency: --start-time→--after--end-time→--before- Context metadata keys changed:
startTime/endTime→after/before - Note: Interaction object fields (
startTime/endTime) are unchanged - BREAKING: CLI:
interaction lsnow requires--type(was optional but API required it). - CLI:
interaction lsdate range now defaults to all-time when--daysand--afterare omitted. - BREAKING: CLI:
interaction lsremoved--cursorand--allflags (auto-chunking replaces manual pagination). - BREAKING: CLI:
interaction lsoutput field renamedmodifiers.type→modifiers.types(now always an array). - BREAKING: CLI:
interaction lsmetadatachunksProcessedmoved totypeStats[type].chunksProcessed. - BREAKING: CLI: Naive datetime strings (without timezone) are now interpreted as local time instead of UTC. Use explicit
Zsuffix or offset for UTC. See datetime-handling guide for details. - BREAKING: CLI: List entry field commands unified into single
entry fieldcommand: entry set-field --field F --value V→entry field --set F Ventry set-field --field F --value-json '{...}'→entry field --set-json '{"F": ...}'entry set-fields --updates-json '{...}'→entry field --set-json '{...}'entry unset-field --field F→entry field --unset Fentry unset-field --field F --value V→entry field --unset-value F Ventry unset-field --field F --all-values→entry field --unset F- Removed:
--field-idoption (field IDs can be passed as FIELD argument directly) - Behavior change:
--seton multi-value field now replaces all values (use--appendto add)
Added¶
- CLI:
interaction ls --typenow accepts multiple types (e.g.,--type email --type meeting). - CLI:
interaction ls --type allconvenience option fetches all interaction types. - CLI:
interaction lsmulti-type results sorted by date descending (types interleaved). - CLI:
interaction lsmetadata includestypeStatswith per-type counts and chunk info. - CLI:
interaction lsauto-chunking for date ranges > 1 year (transparently splits into API-compatible chunks). - CLI:
interaction ls --days Nconvenience flag for "last N days" queries. - CLI:
interaction ls --csvand--csv-bomflags for CSV export. - CLI:
interaction lsmetadata in JSON output includesdateRange,typeStats,totalRows. - SDK:
ListEntryService.from_saved_view()now acceptsfield_idsandfield_typesparameters. - CLI:
list export --saved-viewcan now be combined with--fieldfor server-side filtering with explicit field selection. - SDK:
idsparameter added toPersonService,CompanyService, andOpportunityServicefor batch fetching by ID. - CLI:
entry field --get FIELDfor reading field values (new functionality). - CLI:
entry field --append FIELD VALUEfor adding to multi-value fields without replacing. - CLI:
entry field --unset-value FIELD VALUEfor removing specific value from multi-value field.
Fixed¶
- SDK:
FieldValuesnow properly parses field arrays from API responses (previously showedrequested=False).
Removed¶
- CLI:
person searchandcompany searchcommands. Useperson ls --queryandcompany ls --queryinstead. - CLI: Removed
entry set-field,entry set-fields, andentry unset-fieldcommands (replaced by unifiedentry field).
0.7.0 - 2026-01-08¶
Added¶
- CLI: Column limiting for wide table output - tables now auto-limit columns based on terminal width.
- CLI:
--all-columnsflag to show all columns regardless of terminal width. - CLI:
--max-columns Nflag for fine control over column limits. - CLI: Real-time filter scanning progress during
list export --filtershows "Scanning X... (Y matches)". - CLI: Export summary line after filtered operations (e.g., "Exported 35 rows (filtered from 9,340 scanned) in 2:15").
- CLI:
format_duration()helper for human-readable time formatting. - CLI: Rich pager now uses
styles=Trueto preserve ANSI colors when paging. - SDK:
FilterStatsdataclass for tracking scanned/matched counts during filtered pagination. - SDK:
PaginatedResponse.filter_statsproperty exposes filter statistics.
Fixed¶
- CLI: JSON progress output no longer appears alongside Rich progress bar (mutual exclusivity enforced).
- SDK: Dropdown field filtering now extracts "text" property from dropdown dicts.
0.6.11 - 2026-01-07¶
Added¶
- CLI: Parameter help text (
help) now included in--help --jsonoutput. - CLI: Click.Choice values (
choices) now included in--help --jsonoutput. - CLI: Examples from docstrings now parsed and included in
--help --jsonoutput.
Changed¶
- CLI: Improved
--filterhelp text with full operator list (= != =~ =^ =$ > < >= <=). - CLI: Improved
--queryhelp text to clarify V1 fuzzy search vs V2 structured filtering.
0.6.10 - 2026-01-06¶
Added¶
- CLI: JSON progress output to stderr when not connected to a TTY (for MCP integration).
- CLI:
@progress_capabledecorator to mark commands supporting progress reporting. - CLI: Rate-limited progress updates (0.65s interval) with guaranteed 100% completion emission.
- CLI:
progressCapablefield in--help --jsonoutput for registry generation. - MCP:
run_xaffinity_with_progresshelper for progress-aware CLI execution. - MCP:
command_supports_progresshelper to check registry for progress capability. - MCP: Execute tools now forward CLI progress for
@progress_capablecommands. - CLI: File upload commands (
person/company/opportunity files upload) marked as@progress_capable. - MCP:
PROGRESS_MIN_VERSIONin COMPATIBILITY for graceful degradation with older CLIs. - MCP:
version_gtehelper for portable version comparison (macOS/Linux). - MCP: CLI version check in
command_supports_progress(disables progress for CLI < 0.6.10). - MCP:
XAFFINITY_CLI_VERSIONexported for tool scripts to access CLI version.
Changed¶
- MCP: Updated mcp-bash.lock to patched v0.9.3 (commit ee245a7) with progress passthrough fixes.
0.6.9 - 2026-01-06¶
Changed¶
- CLI Plugin: Skill now documents destructive command confirmation flow (look up, ask, wait, execute with
--yes). - CLI Plugin: Skill lists all destructive commands requiring double confirmation.
0.6.8 - 2026-01-05¶
Added¶
- CLI:
@categoryand@destructivedecorators for MCP registry generation. - CLI:
--help --jsonoutput for machine-readable command documentation. - CLI: Commands now expose category (read/write/local) and destructive metadata.
0.6.7 - 2026-01-03¶
Changed¶
- Docs: Updated all documentation links to use versioned
/latest/URLs for reliable navigation.
0.6.6 - 2026-01-03¶
Fixed¶
- SDK: Regex patterns in
lists.pyandhttp.pywere double-escaped, matching literal\dinstead of digits.
0.6.5 - 2026-01-02¶
No user-facing changes. Version bump for PyPI release.
0.6.4 - 2026-01-02¶
No user-facing changes. Version bump for PyPI release.
0.6.3 - 2026-01-02¶
No user-facing changes. Version bump for PyPI release.
0.6.2 - 2026-01-02¶
Fixed¶
- CLI: Added explicit
type=strto Click arguments for Python 3.13 mypy compatibility.
0.6.1 - 2026-01-02¶
Changed¶
- Docs: Separated MCP Server documentation from Claude Code plugins.
0.6.0 - 2026-01-02¶
Added¶
- MCP:
read-xaffinity-resourcetool for clients with limited resource support.
Changed¶
- Plugins: Restructured into 3-plugin marketplace architecture (affinity-sdk, xaffinity-cli, xaffinity-mcp).
- Docs: Restructured Claude integrations with consistent naming.
Fixed¶
- CLI: Improved
setup-keycommand UX with Rich styling. - MCP: Source both
.zprofileand.zshrcin environment wrapper. - MCP: Parse JSON response correctly for
check-keyoutput.
0.5.1 - 2026-01-01¶
Fixed¶
- Plugins: Consolidated plugin structure and fixed relative paths.
0.5.0 - 2026-01-01¶
Added¶
- MCP: Initial xaffinity MCP server as separate Claude Code plugin.
- CLI: Top-level
entrycommand group as shorthand forlist entry(e.g.,xaffinity entry getinstead ofxaffinity list entry get). - CLI:
--query/-qflag forperson ls,company ls, andopportunity lsto enable free-text search (V1 API). - CLI:
--company-idand--opportunity-idoptions forinteraction ls. - CLI:
-Ashort flag for--allon all paginated list commands. - CLI:
-nshort flag for--max-resultson all commands with result limits. - CLI:
-sshort flag for--page-sizeon all pagination commands. - CLI:
-tshort flag for--typeon interaction commands. - CLI: Structured
CommandContextfor all commands. - SDK:
OpportunityService.search(),search_pages(),search_all()methods for V1 opportunity search. - SDK: Async versions of opportunity search methods in
AsyncOpportunityService. - SDK:
InteractionService.list()now acceptscompany_idandopportunity_idparameters.
Changed¶
- CLI:
list viewrenamed tolist getfor consistency with other entity commands. - CLI:
--completed/--not-completedboolean flag pattern forreminder update(replaces separate flags). - CLI: Removed API version mentions from help text (implementation detail).
- CLI:
interaction lsnow requires an entity ID (--person-id,--company-id, or--opportunity-id) and defaults to last 7 days with visible warning (API max: 1 year). - CLI: Unified
person field,company field,opportunity fieldcommands replaceset-field,set-fields, andunset-fieldcommands. New syntax:--set FIELD VALUE,--unset FIELD,--set-json '{...}',--get FIELD. - CLI: Note content separated from metadata in table display.
Removed¶
- CLI:
person set-field,person set-fields,person unset-fieldcommands (useperson fieldinstead). - CLI:
company set-field,company set-fields,company unset-fieldcommands (usecompany fieldinstead). - CLI:
opportunity set-field,opportunity set-fields,opportunity unset-fieldcommands (useopportunity fieldinstead).
Fixed¶
- CLI: Help text formatting - added missing spaces in command examples (~78 instances).
- CLI: Improved
--cursorhelp text explaining incompatibility with--page-size. - CLI: Clarified
--csvhelp text to indicate it writes to file while stdout format is unchanged. - CLI: CommandContext validation and test isolation issues.
0.4.8 - 2025-12-31¶
Added¶
- CLI:
xaffinity field historyfor viewing field value change history. - CLI: Session caching for pipeline optimization via
AFFINITY_SESSION_CACHEenvironment variable. - CLI:
session start/end/statuscommands for managing session cache lifecycle. - CLI:
--session-cacheand--no-cacheglobal flags for cache control. - CLI: Cache hit/miss visibility with
--traceflag. - CLI:
config check-key --jsonnow includespatternfield showing key source. - SDK: Client-side filtering for list entries (V2 API does not support server-side filtering).
Changed¶
- CLI:
--filteron list entry commands now applies client-side with warning (V2 API limitation). - CLI: Removed
--opportunity-idfromlist entry add(opportunities are created atomically viaopportunity create --list-id).
Fixed¶
- SDK: Client-side filter parsing handles whitespace-only and unparseable filters gracefully.
- CLI:
--filteron list entries now returns proper field values (V2 API format).
0.4.0 - 2025-12-30¶
Added¶
- CLI:
config check-keycommand to check if an API key is configured (checks environment, .env, and config.toml). - CLI:
config setup-keycommand for secure API key configuration with hidden input, validation, and automatic .gitignore management. - CLI:
set-field,set-fields,unset-fieldcommands for person, company, opportunity, and list entry entities. - CLI:
list entry getcommand with field metadata display. - CLI: Enhanced
--expand-filtersyntax with OR (|), AND (&), NOT (!), NULL checks (=*,!=*), and contains (=~). - SDK:
list_entriesfield added toPersonmodel. - SDK: Unified filter parser with
parse()function andmatches()method for client-side filter evaluation.
Changed¶
- CLI: Authentication error hints now reference
config check-keyandconfig setup-keycommands. - CLI: Authentication documentation updated with Quick Setup section.
Fixed¶
- CLI: Default
--page-sizereduced from 200 to 100 to match Affinity API limit. - SDK: Async
merge()parameter names corrected (primaryCompanyId/duplicateCompanyId). - SDK: Cache invalidation added to async create/update/delete in
CompanyService.
Removed¶
- CLI: Deprecated
field-valueandfield-value-changescommand groups removed (use entity-specific field commands instead). - CLI: Deprecated
update-fieldandbatch-updatelist entry commands removed (useset-field/set-fieldsinstead).
0.3.0 - 2025-12-30¶
Added¶
- CLI:
xaffinity list export --expandfor exporting list entries with entity field expansion (company/person/opportunity fields). - CLI:
xaffinity field-value-changes lsfor viewing field value change history. - CLI:
xaffinity company get(id/URL/resolver selectors) with--all-fieldsand--expand lists|list-entries|people. - CLI:
xaffinity person get(id/URL/resolver selectors) with--all-fieldsand--expand lists|list-entries. - CLI:
xaffinity person lsandxaffinity company lswith search flags. - CLI:
xaffinity opportunitycommand group withls/get/create/update/delete. - CLI:
xaffinity note,xaffinity reminder, andxaffinity interactioncommand groups. - CLI:
xaffinity file uploadcommand for file uploads. - CLI: Write/merge/field operations for list entries.
- CLI:
--max-resultsand--allcontrols for pagination and expansions. - CLI: Progress reporting for all paginated commands.
- CLI: Rate limit visibility via SDK event hook.
- CLI:
--traceflag for debugging SDK requests. - SDK:
client.files.download_stream_with_info(...)exposes headers/filename/size alongside streamed bytes. - SDK: v1-only company association helpers
get_associated_person_ids(...)andget_associated_people(...). - SDK: List-scoped opportunity resolution helpers
resolve(...)andresolve_all(...). - SDK: Async parity for company and person services.
- SDK: Async parity for V1-only services.
- SDK: Async list and list entry write helpers.
- SDK: Pagination support for person resolution in
PersonServiceandAsyncPersonService. - SDK:
client.clear_cache()method for cache invalidation. - SDK: Field value changes service with
client.field_value_changes. - SDK: Detailed exception handling for
ConflictError,UnsafeUrlError, andUnsupportedOperationError. - SDK: Webhook
sent_attimestamp validation. - SDK: Request pipeline with policies (read-only mode, transport injection).
- SDK:
on_errorhook for error observability. - Inbound webhook parsing helpers:
parse_webhook(...),dispatch_webhook(...), andBodyRegistry. - Claude Code plugin for SDK/CLI documentation and guidance.
Changed¶
- CLI: Enum fields now display human-readable names instead of integers (type, status, direction, actionType).
- CLI: Datetimes render in local time with timezone info in column headers.
- CLI: Human/table output renders dict-shaped results as sections/tables (no JSON-looking panels).
- CLI:
--jsonoutput now uses section-keyeddataandmeta.pagination. - CLI: List-entry fields tables default to list-only fields; use
--list-entry-fields-scope allfor full payloads. - CLI: Domain columns are now linkified in table output.
- CLI: Output only pages when content would scroll.
FieldValueTypeis now V2-first and string-based (e.g.dropdown-multi,ranked-dropdown,interaction).ListEntry.entityis now discriminated byentity_type.- Rate limit API unified across sync and async clients.
Fixed¶
- SDK:
ListService.get()now uses V1 API to return correctlist_size. - CLI: JSON serialization now handles datetime objects correctly.
- Sync entity file download
deadline_secondshandling. - File downloads now use public services for company expansion pagination.
0.2.0 - 2025-12-17¶
Added¶
- Initial public release.
client.files.download_stream(...)andclient.files.download_to(...)for chunked file downloads.client.files.upload_path(...)andclient.files.upload_bytes(...)for ergonomic uploads.client.files.all(...)/client.files.iter(...)for auto-pagination over files.
Changed¶
- File downloads now follow redirects without forwarding credentials and use the standard retry/diagnostics policy.
client.files.list(...)andclient.files.upload(...)now require exactly one ofperson_id,organization_id, oropportunity_id(per API contract).