@audienti/cli 0.2.0-exora.3 → 0.2.0-exora.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +2 -2
  2. package/cli/account-reply-operations.mjs +24 -1
  3. package/cli/agents.mjs +42 -10
  4. package/cli/audience-operations.mjs +1 -1
  5. package/cli/audienti.mjs +54 -17
  6. package/cli/campaign-find.mjs +155 -0
  7. package/cli/campaign-operations.mjs +6 -3
  8. package/cli/campaigns.mjs +21 -3
  9. package/cli/cohort-operations.mjs +1 -1
  10. package/cli/discovery-operations.mjs +2 -1
  11. package/cli/discovery.mjs +3 -2
  12. package/cli/json-file.mjs +1 -1
  13. package/cli/kv-operations.mjs +17 -0
  14. package/cli/kv.mjs +58 -0
  15. package/cli/mcp.mjs +2 -2
  16. package/cli/outcome-operations.mjs +19 -0
  17. package/cli/outcomes.mjs +16 -0
  18. package/cli/owner-account-client.mjs +1 -1
  19. package/cli/{approach-operations.mjs → playbook-operations.mjs} +15 -15
  20. package/cli/progress.mjs +6 -6
  21. package/cli/question-operations.mjs +4 -4
  22. package/cli/questions.mjs +4 -4
  23. package/cli/queue-operations.mjs +7 -6
  24. package/cli/queue.mjs +4 -3
  25. package/cli/recipient-operations.mjs +1 -1
  26. package/cli/rooms-operations.mjs +6 -5
  27. package/cli/rooms.mjs +7 -6
  28. package/cli/secret-operations.mjs +7 -0
  29. package/cli/secrets.mjs +30 -0
  30. package/cli/sender-account-operations.mjs +6 -2
  31. package/cli/sender-accounts.mjs +13 -8
  32. package/cli/skills-mcp.mjs +6 -2
  33. package/cli/skills.mjs +55 -12
  34. package/cli/tools-operations.mjs +18 -10
  35. package/cli/tools.mjs +48 -20
  36. package/cli/work-execution-operations.mjs +6 -0
  37. package/cli/work-execution.mjs +3 -3
  38. package/cli/work-operations.mjs +4 -3
  39. package/cli/work.mjs +2 -2
  40. package/cli/workspaces.mjs +28 -16
  41. package/package.json +2 -2
  42. package/skills/agent-rooms/SKILL.md +104 -0
  43. package/skills/agent-rooms/references/commands.md +58 -0
  44. package/skills/audienti/SKILL.md +106 -5
  45. package/skills/audienti/references/helpers.md +15 -2
  46. package/skills/audienti/references/profile-workflows.md +4 -2
  47. package/skills/audienti/scripts/contracts.mjs +1 -1
  48. package/skills/getting-started/SKILL.md +180 -154
  49. package/skills/getting-started/references/audience.md +72 -7
  50. package/skills/getting-started/references/campaign-draft.md +54 -13
  51. package/skills/getting-started/references/capabilities.md +214 -0
  52. package/skills/getting-started/references/mailbox-check.md +64 -0
  53. package/skills/getting-started/references/outcome-checks.md +89 -0
  54. package/skills/getting-started/references/replies.md +152 -0
  55. package/skills/getting-started/references/reply-checks.md +54 -0
  56. package/skills/playbook-design/SKILL.md +132 -0
  57. package/skills/playbook-design/references/fields.md +89 -0
  58. package/source-manifest.json +58 -43
  59. package/skills/getting-started/references/inventory.md +0 -52
@@ -1,23 +1,44 @@
1
1
  # Campaign draft
2
2
 
3
- Fill this before you ask the owner anything. Mark each value with where it came from:
4
- `found` (quote or link), `inferred` (say from what) or `needed` (ask the owner).
3
+ Fill this before you ask the user anything. Mark each value with where it came from:
4
+ `found` (quote or link), `inferred` (say from what) or `needed` (ask the user).
5
5
 
6
6
  | Field | Where it goes | How to fill it |
7
7
  | --- | --- | --- |
8
- | Offer: what you sell | Offer | Website, pitch deck, proposals, email signature. Owner's words. |
9
- | Offer: problem it solves | Offer | Website, sent email, call notes. Owner's words. |
10
- | Offer: proof point | Offer | Only a result or customer the owner already states publicly or confirms. |
8
+ | Offer: what you sell | Offer | Website, pitch deck, proposals, email signature. User's words. |
9
+ | Offer: problem it solves | Offer | Website, sent email, call notes. User's words. |
10
+ | Offer: proof point | Offer | Only a result or customer the user already states publicly or confirms. |
11
11
  | Company type, size, region | Audience filters | Current customers in the CRM or sent email; check values with `audience options`. |
12
- | Buyer title and seniority | Audience filters | Who replies to the owner or signs deals. |
12
+ | Buyer function, seniority and title | Audience filters | The function that owns the problem the offer solves, then who replies to the user or signs deals. Always set a function. |
13
13
  | Who to avoid | Do-not-contact list | Current customers, partners, competitors (`dnc add` or `dnc import`). |
14
- | Premise | Campaign premise | One sentence: why these people, why now. |
15
- | Approach | Campaign approach pin | One published approach from `approach list`, with one line on why. |
14
+ | Signals | Campaign `signals` | At least one. Events that make a company worth contacting now: hiring for a role, funding, a new leader, a tool change. Each one is a yes/no `research` question ([audience definition](audience.md) section 5). Never put these in the audience. |
15
+ | Premise | Campaign premise | One sentence: why these people, why now. It names the signal and why the signal means they need the offer. |
16
+ | Playbook | Campaign playbook pin | One playbook from `playbook list`, or a new one drafted with the playbook-design skill, with one line on why. When the list returns a `recommendation`, start from that starter playbook and name it with its `reason` in one line. |
16
17
  | Channels | Campaign channels | `email` and `linkedin`, only for accounts you reported. |
17
- | Test length | Campaign `schedule.endAt`, `schedule.maxCompanies` | 30 days from activation, or a company cap. |
18
- | Success measure | Approach goal stage | Meetings booked. |
18
+ | Pace | Campaign pace | New people a day. Take the number and its reason from the **Pace line** in your queue task for the channels you propose: 25 a day per confirmed LinkedIn account, or the campaign's own number (50 by default) for email only. Write it as `Pace: 25 a day · 1 LinkedIn account · about 100 people a week`. Once the campaign is saved, `audienti campaign pace ID` returns its own line: use that line, so the card and the campaign page show the same number. The user can change it in their answer. |
19
+ | Test length | Campaign `schedule.endAt`, `schedule.maxCompanies` | 30 days from activation or a company cap, whichever comes first. |
20
+ | Success measure | Playbook goal stage | Meetings booked. |
21
+ | Waits | Playbook step `wait` | What each step waits for (`accept`, `reply`, `meeting` or `timeout`) and for how many days; their sum fits the test length. A wait starts only when the agent reports a succeeded result for the plan action that carries the step's key as `playbookStepKey`, and any reply ends every wait (playbook-design `references/fields.md`). |
19
22
 
20
- Ask for at most two `needed` values in your first message. Fill the rest after the owner
23
+ ## Signals: the through line
24
+
25
+ Prospect finding starts from signals: they find the companies that need the offer
26
+ today. A campaign with no signal and no target list cannot start, so every campaign you
27
+ draft has at least one. A signal is not "they might want to buy". It is an
28
+ observable event that shows a company has the problem the offer solves, now. Build the
29
+ line in this order:
30
+
31
+ 1. **Offer**: what it solves. "An outbound agency books meetings for B2B software teams."
32
+ 2. **Problem**: who has it. "The company needs pipeline and has no working outbound team."
33
+ 3. **Signal**: the event that shows the problem today. "The company is hiring SDRs or
34
+ BDRs": it has decided to invest in outbound but does not have the team yet.
35
+ 4. **Premise**: one sentence that joins them. "Companies hiring their first SDRs need
36
+ pipeline before the hires ramp; the agency books meetings in weeks."
37
+
38
+ If you cannot name the event that shows the problem, ask the user what tells them a
39
+ company needs the offer now. Do not leave the signals empty.
40
+
41
+ Ask for at most two `needed` values in your first message. Fill the rest after the user
21
42
  replies.
22
43
 
23
44
  Name the campaign after the buyer and the test, for example "Heads of finance,
@@ -31,7 +52,27 @@ name or notes.
31
52
  | Offer | `audienti offer create --file OFFER.json --request-id UUID` | `offer_create` |
32
53
  | Audience | `audienti audience create --file AUDIENCE.json --request-id UUID` | `audience_create` |
33
54
  | Campaign | `audienti campaign create --file CAMPAIGN.json --request-id UUID` | `campaign_create` |
55
+ | Pace | `audienti campaign pace ID --per-day derived`, or `--per-day N` with the user's number | `campaign_pace_set` |
34
56
  | Next test | `audienti campaign successor ID --file CAMPAIGN.json --request-id UUID` | `campaign_successor` |
35
57
 
36
- Run each command without arguments to see its fields, or read the API schema at
37
- `/api/v1/openapi.json`.
58
+ Read each command's request fields in the API schema at `/api/v1/openapi.json`
59
+ (search for the MCP tool name above). A campaign also needs the `audienceRevision`
60
+ and `offerRevision` returned when you saved the audience and offer.
61
+
62
+ ## Example proposal
63
+
64
+ The shape of a first proposal, not fixed copy:
65
+
66
+ > I'm Arthur, your prospecting agent in Audienti. I find people who fit, research them
67
+ > and draft first messages for you to review. I can use Gmail (read, draft), LinkedIn
68
+ > through Composio, and web search. No CRM is connected, so records stay in Audienti.
69
+ > Proposed first test, 30 days. Offer: late-invoice recovery for logistics firms (from
70
+ > your website). Audience: finance function, Director and above, logistics companies,
71
+ > 51 to 500 staff, UK (region inferred from your last 20 sent emails). Smart rule: the
72
+ > company runs its own truck or freight fleet. Signal: the company is hiring a credit
73
+ > controller or accounts receivable clerk. Premise: a company hiring for collections
74
+ > has late invoices piling up now, and recovery gets cash back faster than a new hire. Playbook: problem-led email, then LinkedIn
75
+ > follow-up. Pace: 25 a day · 1 LinkedIn account · about 100 people a week. Success:
76
+ > meetings booked. Two questions: is the UK the right first region, and is there a
77
+ > customer result I may cite? Answer "Go" to save it as a draft, or write your changes.
78
+ > Nothing is sent until you approve it.
@@ -0,0 +1,214 @@
1
+ # Capability report format
2
+
3
+ Report what you can do for the user right now, as rows. One row per capability,
4
+ provider, source and account. The capability ids come from Audienti's catalog
5
+ ([ADR 015](https://github.com/omalab/exora/blob/main/docs/decisions/015-capability-taxonomy.md));
6
+ the full list is below.
7
+
8
+ ```json
9
+ {
10
+ "runtime": { "name": "Claude Code", "model": "claude-opus-5-5" },
11
+ "capabilities": [
12
+ { "capability": "web.search", "provider": "web", "via": "native", "access": "read", "status": "connected" },
13
+ { "capability": "web.page.read", "provider": "web", "via": "native", "access": "read", "status": "connected" },
14
+ { "capability": "profile.message.send", "provider": "email", "via": "composio:gmail", "account": "owner@example.com", "access": "send", "status": "connected" },
15
+ { "capability": "profile.inbox.read", "provider": "email", "via": "composio:gmail", "account": "owner@example.com", "access": "read", "status": "connected" },
16
+ { "capability": "profile.read", "provider": "linkedin", "via": "composio:linkedin", "access": "read", "status": "connected" },
17
+ { "capability": "profile.message.send", "provider": "linkedin", "via": "composio:linkedin", "account": "linkedin.com/in/owner", "access": "send", "status": "connected", "limits": { "daily": 20 } },
18
+ { "capability": "crm.records.search", "provider": "salesforce", "via": "mcp:salesforce", "access": "read", "status": "needs_sign_in" }
19
+ ]
20
+ }
21
+ ```
22
+
23
+ ```sh
24
+ audienti agent capabilities report --file CAPABILITIES.json --profile AGENT_PROFILE
25
+ audienti agent capabilities --profile AGENT_PROFILE
26
+ ```
27
+
28
+ The first command replaces your previous report. The second prints what Audienti now
29
+ counts for you: the rows that work now, and the rows that work with Audienti's hosted
30
+ tools in the selected workspace. Over MCP, use `agent_capabilities_report` and
31
+ `agent_capabilities_list`. Over HTTP, `PUT` and `GET /api/v1/agent-capabilities`.
32
+
33
+ ## Fields
34
+
35
+ - `runtime.name`: your product or runtime as the user knows it. `runtime.model` is
36
+ optional.
37
+ - `capability`: an id from the list below. An unknown id is refused.
38
+ - `provider`: which kind of profile the work touches. Required where the list names
39
+ providers; leave it out where it says "none". Where it says "any", leave it out or
40
+ name the one you use.
41
+ - `via`: where you reach it. `native` for a connector built into your runtime;
42
+ `composio:<app>` (for example `composio:gmail`), `treg:<tool>` or `mcp:<server>` for
43
+ a hub or MCP server. Check your hubs first: list each app connected there as its own
44
+ row.
45
+ - `account`: the user's own mailbox, LinkedIn account or handle you act through, such as
46
+ `owner@example.com` or `linkedin.com/in/owner`. Required for every verb that acts as
47
+ the user (marked "as the user" below).
48
+ - `access`: `read`, `write` or `send`, as listed for the capability. `send` reaches
49
+ another person; drafting is `write`.
50
+ - `status`: `connected` when you can call it now, `needs_sign_in` when it is installed
51
+ but signed out, `unavailable` when it exists but fails.
52
+ - `cost`: leave it out. Your own tools are `owner`.
53
+ - `limits`: optional `daily` and `hourly` caps you respect.
54
+ - `identity`: optional, on an email or LinkedIn row that names an `account`. The
55
+ account's own stable id, as `{ "namespace": "...", "accountId": "..." }`, so Audienti
56
+ can match the LinkedIn account or mailbox to the one the user set up. Use the same identity on
57
+ every row for that account. Examples: `{ "namespace": "linkedin", "accountId":
58
+ "ACoAAB1234" }` for a LinkedIn account, `{ "namespace": "gmail", "accountId":
59
+ "108234567890" }` for a Gmail mailbox.
60
+
61
+ At most 64 rows and 32 KiB.
62
+
63
+ ## LinkedIn accounts
64
+
65
+ For each LinkedIn account you send as, add an entry to `seats` with what you know about
66
+ it. Audienti shows it to the user on the LinkedIn account's page and plans around it.
67
+
68
+ ```json
69
+ {
70
+ "seats": [
71
+ {
72
+ "provider": "linkedin",
73
+ "account": "linkedin.com/in/owner",
74
+ "checkEverySeconds": 21600,
75
+ "inmail": { "available": true, "quotaPerMonth": 15 },
76
+ "limits": { "connectionRequestsPerDay": 20, "connectionRequestsPerWeek": 100, "messagesPerDay": 50, "inmailPerMonth": 15 }
77
+ }
78
+ ]
79
+ }
80
+ ```
81
+
82
+ - `account`: the same LinkedIn account you name in your LinkedIn rows.
83
+ - `checkEverySeconds`: how often you can check this LinkedIn account's sent invitations, at least
84
+ 3600 (one hour). Audienti schedules outcome checks at this cadence; every 6 hours
85
+ when you leave it out. The user never changes it.
86
+ - `inmail`: whether the LinkedIn account can send InMail, and its monthly quota. Report
87
+ `profile.inmail.send` as well; a LinkedIn account counts as InMail-capable only with both.
88
+ - `limits`: the safe limits for this LinkedIn account. Audienti never plans more LinkedIn sends on
89
+ it than its per-day and per-week limits. The user can lower a limit, never
90
+ raise it above the one you report.
91
+
92
+ Report `profile.invitations.read` on the LinkedIn account too, so outcome checks can be assigned to
93
+ you.
94
+
95
+ ## Rules
96
+
97
+ - **Report, never connect.** List only what you can call now. Do not install or sign in
98
+ to anything to fill the list.
99
+ - **Never invent.** Leave out anything you cannot call. A wrong row sends tasks to you
100
+ that you cannot do.
101
+ - **Never report `via: audienti`.** Audienti adds rows for its own hosted tools itself
102
+ when the workspace has credits; a report that sends `audienti` is refused. Leave out
103
+ Audienti's hosted tools unless you reach them through another connector.
104
+ - **Never include secrets** (tokens, cookies, keys) or personal data about the user's
105
+ contacts.
106
+ - Report again when your tools change, and at least every seven days. The user sees
107
+ older rows as stale.
108
+
109
+ ## The catalog
110
+
111
+ Launch rows can be required by queue tasks. Later rows are accepted and shown to the
112
+ user, but no task requires them yet.
113
+
114
+ ### People and companies
115
+
116
+ | Capability | Providers | Access | What it does | Launch |
117
+ | --- | --- | --- | --- | --- |
118
+ | `people.search` | any | read | Find people from audience filters or a question | yes |
119
+ | `people.discover` | any | read | Find look-alikes from a seed person or company | yes |
120
+ | `person.profiles.discover` | any | read | Expand one person into their other profiles | yes |
121
+ | `companies.search` | any | read | Find companies from filters | yes |
122
+ | `company.enrich` | any | read | Fill size, industry, location, site | yes |
123
+ | `company.posts.list` | linkedin, x, instagram, tiktok | read | What a company page has posted | yes |
124
+
125
+ ### Profile: research
126
+
127
+ | Capability | Providers | Access | What it does | Launch |
128
+ | --- | --- | --- | --- | --- |
129
+ | `profile.read` | linkedin, x, instagram, tiktok, github, web | read | Fetch one profile by URL or handle | yes |
130
+ | `profile.enrich` | linkedin, email | read | Fill title, company, location, history from one handle | yes |
131
+ | `profile.find` | email, phone | read | Find a person's handle on this provider | yes |
132
+ | `profile.verify` | email | read | Check the handle is live and deliverable | yes |
133
+ | `profile.posts.list` | linkedin, x, instagram, tiktok | read | What this profile has posted | yes |
134
+ | `profile.comments.list` | linkedin, x, instagram, tiktok | read | Comments this profile wrote on others' posts | yes |
135
+ | `profile.reactions.list` | linkedin, x, instagram, tiktok | read | Reactions this profile gave | yes |
136
+
137
+ ### Profile: as the user (needs `account`)
138
+
139
+ | Capability | Providers | Access | What it does | Launch |
140
+ | --- | --- | --- | --- | --- |
141
+ | `profile.message.send` | email, linkedin, whatsapp, phone, x, instagram | send | Send an approved message | email, linkedin |
142
+ | `profile.inbox.read` | email, linkedin, whatsapp | read | Watch the user's inbox for replies | email, linkedin |
143
+ | `profile.invitations.read` | linkedin | read | Read the user's sent invitations, to see which were accepted or closed | yes |
144
+ | `profile.inmail.send` | linkedin | send | Send an approved InMail | yes |
145
+ | `profile.connection.request` | linkedin | send | Send an approved connection request | yes |
146
+ | `profile.connection.withdraw` | linkedin | send | Withdraw a pending request | yes |
147
+ | `profile.connection.remove` | linkedin | send | Remove a connection | later |
148
+ | `profile.follow` | linkedin, x, instagram, tiktok | send | Follow a person or company | linkedin |
149
+ | `profile.unfollow` | linkedin, x, instagram, tiktok | send | Unfollow | linkedin |
150
+ | `profile.mute` | x, instagram | send | Mute | later |
151
+ | `profile.block` | x, instagram, linkedin | send | Block | later |
152
+ | `profile.call` | phone | send | Call a prospect | later |
153
+
154
+ ### Posts and topics
155
+
156
+ | Capability | Providers | Access | What it does | Launch |
157
+ | --- | --- | --- | --- | --- |
158
+ | `topic.track` | any | read | Track a topic for signals | yes |
159
+ | `posts.search` | linkedin, x, instagram, tiktok, web | read | Find posts about a topic | yes |
160
+ | `post.read` | linkedin, x, instagram, tiktok | read | Fetch one post with its type | yes |
161
+ | `post.comments.list` | linkedin, x, instagram, tiktok | read | Who commented, and what they said | yes |
162
+ | `post.reactions.list` | linkedin, x, instagram, tiktok | read | Who reacted | yes |
163
+ | `post.comment` | linkedin, x, instagram, tiktok | send | Comment as the user (as the user) | later |
164
+ | `post.react` | linkedin, x, instagram, tiktok | send | React as the user (as the user) | later |
165
+ | `post.publish` | linkedin, x, instagram, tiktok | send | Publish a post as the user (as the user) | later |
166
+
167
+ ### Web, calendar, records, owner, integration
168
+
169
+ | Capability | Providers | Access | What it does | Launch |
170
+ | --- | --- | --- | --- | --- |
171
+ | `web.search` | web | read | Search the web | yes |
172
+ | `web.page.read` | web | read | Read one page | yes |
173
+ | `calendar.read` | google, microsoft | read | Free and busy time | later |
174
+ | `calendar.freebusy.read` | google, microsoft | read | Read the user's free and busy times to offer slots in a reply | later |
175
+ | `calendar.event.create` | google, microsoft | write | Create an event and send the invite from the user's calendar | later |
176
+ | `meeting.book` | google, microsoft | write | Book or propose a meeting | later |
177
+ | `meeting.reschedule` | google, microsoft | write | Move a booked meeting | later |
178
+ | `meeting.cancel` | google, microsoft | write | Cancel a booked meeting | later |
179
+ | `crm.records.search` | hubspot, salesforce, pipedrive, other | read | Find records in the user's CRM | yes |
180
+ | `crm.record.read` | hubspot, salesforce, pipedrive, other | read | Read one CRM record | yes |
181
+ | `crm.record.write` | hubspot, salesforce, pipedrive, other | write | Write back to one CRM record | later |
182
+ | `owner.notify` | push, slack, sms, email | send | Reach the user on their own channel | yes |
183
+ | `webhook.receive` | none | read | Take a push instead of polling | later |
184
+ | `webhook.send` | none | send | Post events to the user's URL | later |
185
+
186
+ ### Tools you use for yourself
187
+
188
+ | Capability | Providers | Access | What it does | Launch |
189
+ | --- | --- | --- | --- | --- |
190
+ | `tool.media.transcribe` | none | read | Video or audio to text | later |
191
+ | `tool.image.read` | none | read | Read text and content out of an image | later |
192
+ | `tool.proxy.session.start` | none | write | Sticky residential session for your own browser, metered by bandwidth | yes |
193
+ | `tool.proxy.session.rotate` | none | write | New IP on the current session | yes |
194
+ | `tool.proxy.config.read` | none | read | Read region, stickiness and bandwidth cap | yes |
195
+ | `tool.proxy.config.write` | none | write | Set region, stickiness and bandwidth cap | yes |
196
+
197
+ Your runtime's documents, spreadsheets, files, code, memory, tasks and chat are not
198
+ capabilities. Leave them out.
199
+
200
+ ## Older reports
201
+
202
+ `audienti agent inventory report` (categories such as `web_search` or `crm`) and the
203
+ per-account `audienti agent capabilities report` with `provider` and `accountRef` are
204
+ still accepted for one release. Audienti translates both into rows. Send the format above
205
+ instead.
206
+
207
+ Translated inventory reports match fewer queue tasks than before:
208
+
209
+ - `company_data` alone matches none of the find, verify or enrich tasks.
210
+ - `linkedin` alone no longer matches find or enrich tasks.
211
+ - `web_search` alone no longer matches enrich tasks.
212
+ - `people_data` alone no longer matches verify tasks.
213
+ - `email` and `linkedin` match send tasks only for their own provider and only with
214
+ `send` access; `social` matches no send task.
@@ -0,0 +1,64 @@
1
+ # The mailbox test email (`mailbox.confirm`)
2
+
3
+ Audienti never signs in to your user's mailbox. You send from it through your own email
4
+ connector (Gmail or Outlook, through Composio, a Claude connector or similar), so you also
5
+ prove it works. A mailbox counts for campaigns only once you sent a test email from it to
6
+ itself, read it back and reported the code. Your user does nothing in this check: never ask
7
+ them for the code, and never send them to Settings to confirm a mailbox.
8
+
9
+ When your user connects a mailbox you reported, or asks for another test email in Settings →
10
+ Sending, you get one `mailbox.confirm` task for it. Its `detail.test` gives the mailbox as
11
+ you reported it (`mailbox`), its address as Audienti has it (`address`, or `null` when the
12
+ mailbox was reported under a name or an account id that is no address), the code (`code`) and
13
+ the subject to use (`subject`, `Audienti mailbox check` and the code).
14
+
15
+ 1. Claim the task: `audienti queue claim TASK_ID`.
16
+ 2. Send one email from that mailbox to its own address, and to nobody else. Use the address
17
+ your connector reports for the mailbox; `address` is only a hint, and if your connector
18
+ reports another, use that one. Use the subject the task gives and put the code in the body
19
+ too. Send nothing else.
20
+ 3. Read that mailbox's inbox with your connector until the email arrives: about once a
21
+ minute, for up to 15 minutes. Read only the test email. Note its message id and the time
22
+ it arrived.
23
+ 4. Report what you read, with the address you sent it to (`sentTo`):
24
+
25
+ ```json
26
+ { "results": [{ "kind": "mailbox.received", "code": "CODE", "sentTo": "ADDRESS_YOU_SENT_TO", "message": "MESSAGE_ID", "receivedAt": "2026-11-04T12:01:00Z" }] }
27
+ ```
28
+
29
+ ```sh
30
+ audienti outcomes report TASK_ID --file RESULTS.json
31
+ ```
32
+
33
+ 5. Delete both copies of the test email, the sent one and the received one, then report it:
34
+ `{ "results": [{ "kind": "mailbox.deleted", "message": "MESSAGE_ID" }] }`.
35
+ 6. Report the task done: `audienti queue done TASK_ID --note "test email confirmed"`.
36
+
37
+ Use the code exactly as you read it back from the email; the dash and case do not matter.
38
+ Only the task's own code confirms the mailbox. A code of an older test email is refused
39
+ (`stale_code`), any other code too (`wrong_code`), and so is an email that arrived before
40
+ the task asked for it (`received_before_asked`), and so is an email sent to another of your
41
+ user's mailboxes (`other_mailbox`). Reporting the same email again returns
42
+ `already_recorded`.
43
+
44
+ You can do this check even if you can only read the workspace: your own test email is the
45
+ one result a read-only agent may report.
46
+
47
+ ## When you cannot send or read
48
+
49
+ Report which step failed, with a short reason, then report the task done:
50
+
51
+ ```json
52
+ { "results": [{ "kind": "mailbox.failed", "step": "send", "reason": "No Gmail connector" }] }
53
+ ```
54
+
55
+ - `send`: you have no connector that can send from the mailbox, or the send failed. Tell
56
+ your user exactly: "I need a Gmail or Outlook connector to send from your mailbox."
57
+ - `read`: the email went out but you cannot read the mailbox's inbox, or it did not arrive
58
+ in 15 minutes. Tell your user exactly: "I need a Gmail or Outlook connector that can read
59
+ your mailbox's inbox."
60
+
61
+ Settings → Sending and Capabilities then show your user which step failed. No new task comes
62
+ until they fix your connector and choose Send another test email. The task cannot be
63
+ reported done until you reported `mailbox.received` or `mailbox.failed`
64
+ (`mailbox_not_reported`).
@@ -0,0 +1,89 @@
1
+ # Outcome checks
2
+
3
+ An outcome check is a queue task (`outcomes.check`) that asks you whether the people
4
+ you invited on one of the user's LinkedIn accounts have accepted. Audienti schedules it
5
+ for you at the cadence you reported for that LinkedIn account (`checkEverySeconds`, every 6 hours
6
+ when you report none), and only while someone on that LinkedIn account waits for an accept. Each
7
+ task lists at most 100 people, so a LinkedIn account with more people waiting gets several tasks
8
+ in the same interval, and every waiting person is on one of them. You never decide when to check, and you never check
9
+ anyone the task does not list.
10
+
11
+ ## What the task gives you
12
+
13
+ The task's `detail` holds:
14
+
15
+ - `seat`: the LinkedIn account to check (`account` is the LinkedIn account you reported, for example
16
+ `linkedin.com/in/owner`).
17
+ - `waiting`: the people to check, at most 100. The list stays as it is while you hold
18
+ the task, so you can report on everyone it lists; no other check lists them meanwhile,
19
+ even one for the next interval. Each has `person` (the Audienti person
20
+ id you report against), `name`, `profileUrl` and `invitedAt` (when the invitation was
21
+ sent).
22
+
23
+ The task requires `profile.invitations.read` on that LinkedIn account. If you hold it, the task is
24
+ assigned to you.
25
+
26
+ ## The procedure
27
+
28
+ 1. Claim the task: `audienti queue claim TASK_ID`.
29
+ 2. Read the LinkedIn account's own sent invitations with your tool, or read each listed person's
30
+ profile to see the connection degree. Look up only the people in `waiting`. Do not
31
+ list, search or open anyone else, and do not page through the user's whole network.
32
+ 3. Keep to the limits you reported for this LinkedIn account. Spread reads out if your tool asks
33
+ you to. If you cannot check now (signed out, rate limited), release the task with a
34
+ note: `audienti queue release TASK_ID --note "why"`.
35
+ 4. Write what you found to a file and report it:
36
+
37
+ ```json
38
+ {
39
+ "results": [
40
+ { "kind": "linkedin.accept", "person": "PERSON_ID", "at": "2026-11-01T09:30:00Z" },
41
+ { "kind": "linkedin.invite_closed", "person": "PERSON_ID", "reason": "withdrawn" },
42
+ { "kind": "linkedin.profile_degree", "person": "PERSON_ID", "degree": 2 }
43
+ ]
44
+ }
45
+ ```
46
+
47
+ ```sh
48
+ audienti outcomes report TASK_ID --file RESULTS.json
49
+ ```
50
+
51
+ 5. Report the task done: `audienti queue done TASK_ID --note "3 checked, 1 accepted"`.
52
+
53
+ ## Result kinds
54
+
55
+ - `linkedin.accept {person, at}`: the person accepted. `at` is when they accepted, as
56
+ close as your tool tells you. Audienti ends the person's wait and plans the next step
57
+ of the playbook, which appears in your queue.
58
+ - `linkedin.invite_closed {person, reason}`: the invitation is no longer pending.
59
+ `reason` is `withdrawn` (the user or you withdrew it) or `expired` (LinkedIn closed
60
+ it). Audienti records the fact; the wait still ends at its timeout.
61
+ - `linkedin.profile_degree {person, degree}`: optional evidence, the connection degree
62
+ you read on the profile (1, 2 or 3). A 1 means connected; report it as an accept as
63
+ well when you know the time.
64
+
65
+ Leave out anyone still pending. They stay on the next check until they accept or the
66
+ wait times out.
67
+
68
+ ## What the report returns
69
+
70
+ `results` has one entry per result you sent, in the same order. Each person is recorded
71
+ on their own, so one person who cannot be recorded does not hold back the others.
72
+
73
+ - `status`: `recorded` (written now), `already_recorded` (an earlier report wrote it;
74
+ `eventId` is that earlier event) or `not_recorded` (nothing was written).
75
+ - `reason`: why, when there is one. `participation_ended` or
76
+ `participation_unavailable` means the fact was recorded but nobody moves, because
77
+ the person's campaign participation ended or its playbook cannot be read.
78
+ `not_found` with `not_recorded` means the person is no longer in the workspace.
79
+ - `next`: the person's next step after the report, when there is one.
80
+ - `failed`: the accept was recorded, but planning the next step failed. Audienti shows
81
+ the failure on the person's timeline; `reason` holds its code. Sending the same
82
+ accept again returns the same failure until it is resolved.
83
+
84
+ ## Errors
85
+
86
+ - `not_claimed`: claim the task first, or your lease expired. Read the queue again.
87
+ - `not_on_list`: you reported a person the task does not list. Report only the listed
88
+ people.
89
+ - `validation_error`: a result has a wrong kind, person id or time.
@@ -0,0 +1,152 @@
1
+ # Handling a reply
2
+
3
+ A reply task (`replies.handle`) means a prospect answered. The inbound itself is never
4
+ approved: inbound wins, and you record it. What the user approves is your answer.
5
+
6
+ ## What to do
7
+
8
+ 1. Read the conversation: `audienti prospects conversation list PERSON --workspace ID`.
9
+ 2. Classify the reply as one of `interested`, `question`, `later`, `out_of_office`,
10
+ `stop`, `bounce` or `referral`. A stop, a bounce or a referral must carry its typed
11
+ `result`, or the report is refused; see [Stop, bounce and referral](#stop-bounce-and-referral).
12
+ 3. Draft the answer from the mailbox or LinkedIn account the reply came to (the assignee's).
13
+ - Bind the recipient and the sender, or the answer waits in Blocked: the recipient's
14
+ `contactId` comes from `audienti prospects contacts list PERSON`, and the
15
+ `senderAccountId` is the user's confirmed sender account for that mailbox or LinkedIn account
16
+ (`audienti sender accounts list`).
17
+ - If you report `calendar.freebusy.read`, read the user's free times and offer up
18
+ to six slots in the answer. Put the same times in `slots`, so the user sees them on
19
+ the card: "Slots from your calendar · Thu 10:00, 14:30 ET". A scheduling tool that
20
+ reports only `calendar.read` is not the user's calendar and offers no slots.
21
+ - Without a calendar, ask the prospect for a time that suits them, and leave `slots`
22
+ out. The user's card says no agent has a calendar and links to the Capabilities
23
+ screen.
24
+ 4. Record the classification and the draft in one result, keeping the reply open:
25
+
26
+ ```sh
27
+ audienti prospects replies set PERSON --workspace ID --evidence EVIDENCE --file RESULT.json
28
+ ```
29
+
30
+ ```json
31
+ {
32
+ "requestId": "a new UUID for each result",
33
+ "expectedVersion": 0,
34
+ "status": "needs_handling",
35
+ "outcome": null,
36
+ "reason": "Interested and asked for a time on Thursday.",
37
+ "responseEvidenceId": null,
38
+ "classification": "interested",
39
+ "draft": {
40
+ "type": "email.send",
41
+ "content": { "subject": "Re: Late invoices", "body": "Thursday works. I have 10:00 or 14:30 ET. Which suits you?" },
42
+ "recipient": { "prospectId": "PERSON", "email": "jane@example.com", "contactId": "CONTACT" },
43
+ "sender": { "provider": "email", "accountRef": "owner@example.com", "senderAccountId": "SENDER" },
44
+ "reason": "Offers the two free slots on Thursday the prospect asked for.",
45
+ "slots": [
46
+ { "start": "2026-11-05T15:00:00Z", "end": "2026-11-05T15:30:00Z", "timeZone": "America/New_York" },
47
+ { "start": "2026-11-05T19:30:00Z", "end": "2026-11-05T20:00:00Z", "timeZone": "America/New_York" }
48
+ ]
49
+ }
50
+ }
51
+ ```
52
+
53
+ `expectedVersion` is the reply's current version (`audienti prospects replies get`).
54
+ The draft becomes a revision the user reviews in Operator. Nothing is sent until the
55
+ user approves it. A second result for the same reply replaces a draft the user has
56
+ not decided yet.
57
+ 5. Report the task done.
58
+
59
+ The classification records a `replied` event on the person's active campaigns, except
60
+ for `bounce` and `out_of_office`, which are not replies from the person.
61
+
62
+ ## Stop, bounce and referral
63
+
64
+ These three need no decision from the user. Classify the reply, report the typed
65
+ `result` next to the classification it matches, and Audienti records it against the
66
+ person. The classification without its result is refused with 400 naming the result it
67
+ needs (`reply.stop`, `email.bounce` or `reply.referral`). Use the prospect's words,
68
+ verbatim, for `quote`.
69
+
70
+ - **Stop** (`stop`): the prospect asked not to be contacted. Report status `handled`,
71
+ outcome `suppressed` and no draft:
72
+
73
+ ```json
74
+ { "classification": "stop", "status": "handled", "outcome": "suppressed",
75
+ "result": { "type": "reply.stop", "scope": "person", "quote": "Please remove me from your list." } }
76
+ ```
77
+
78
+ The person is suppressed for good on every channel, their pending sends and open
79
+ tasks are withdrawn, and they leave their campaigns. The user cannot override it.
80
+ Use `"scope": "company"` only when they asked to stop for their whole company:
81
+ everyone at the company's domain is suppressed, and so is anyone added later. The
82
+ domain goes on the do-not-contact list marked "Asked to stop", and no one can remove it.
83
+ Never contact a person who asked to stop again, on any channel.
84
+ - **Bounce** (`bounce`): a delivery failure came back instead of a reply. Report
85
+ status `not_reply` and no draft:
86
+
87
+ ```json
88
+ { "classification": "bounce", "status": "not_reply", "outcome": null,
89
+ "result": { "type": "email.bounce", "address": "priya@example.com", "kind": "hard", "evidence": "550 5.1.1 User unknown" } }
90
+ ```
91
+
92
+ `address` is the address that bounced: the person's own address, or one nobody in the
93
+ workspace holds. An address another person holds is refused with 409
94
+ `address_belongs_to_another_person`; report the bounce on the reply that came back
95
+ about that person instead.
96
+ `hard` means the address does not exist or refuses mail: it is marked bounced and
97
+ blocked in every campaign, and the person leaves the campaign as Bounced. `soft`
98
+ means a full mailbox or a temporary failure: the address is held for 24 hours, then
99
+ a retry task asks you to send the message once more. Retried once means exactly one
100
+ send: after the 24 hours the address takes one send, and every other send to it is
101
+ refused with `recipient_contact_bounced` until that send's outcome is known. A second
102
+ bounce within 14 days of the retry counts as hard; with no bounce in that time the
103
+ address opens again. Put the bounce message and its status code in `evidence`.
104
+ - **Referral** (`referral`): the prospect is the wrong person and named someone else.
105
+ Report status `needs_handling` with a thank-you as the draft, and name the person:
106
+
107
+ ```json
108
+ { "classification": "referral", "status": "needs_handling", "outcome": null,
109
+ "result": { "type": "reply.referral", "name": "Sam Lee", "title": "Finance lead",
110
+ "email": "sam@example.com", "quote": "Wrong person, try Sam Lee in finance." },
111
+ "draft": { "type": "email.send", "content": { "subject": "Re: Late invoices", "body": "Thank you, Jane. I will reach out to Sam." }, "...": "recipient, sender and reason as above" } }
112
+ ```
113
+
114
+ Give `linkedinUrl` or `email` when the prospect gave them. Audienti finds or adds the
115
+ named person (an email someone in the workspace already holds names that person), links
116
+ the two records, and leaves the referrer as Not a fit with the note "referred to Sam
117
+ Lee". When the email and the LinkedIn profile belong to different people, the referral
118
+ is refused with 409 `identity_conflict`. Reporting the same referral again, from
119
+ another reply, changes nothing. The named person is not in the queue yet: a
120
+ `prospects.verify` task asks you to check them against the campaign's audience
121
+ first. The thank-you is an answer like any other: the user approves it in Operator
122
+ before it is sent.
123
+
124
+ Every first email you draft ends with one plain sentence that lets the prospect opt
125
+ out, such as "If this is not for you, reply and I will not write again." When they do,
126
+ report it as a stop.
127
+
128
+ ## After the user approves
129
+
130
+ A `work.execute` task with `action: send_reply` follows. Claim it, send the answer in
131
+ the thread the prospect wrote in, report the result, then close the reply with status
132
+ `handled`, outcome `response_recorded` and the sent message's evidence as
133
+ `responseEvidenceId`.
134
+
135
+ ## When a meeting is booked
136
+
137
+ - The prospect picks a slot: send the invite from the user's calendar
138
+ (`calendar.event.create`), then record `"meeting": { "at": "…Z", "source": "sent_invite" }`
139
+ in a `replies set` result.
140
+ - The prospect sent an invite: set `"invite": true` with the classification. A
141
+ `meetings.confirm` task follows, assigned to one agent of the person's assignee that
142
+ is allowed to make workspace changes and reports `calendar.event.create` or
143
+ `meeting.book`. It accepts the invite on the
144
+ assignee's calendar and records `"source": "received_invite"`. If the assignee has no
145
+ such agent, no task appears.
146
+ - The meeting moves the person to the playbook's goal stage. Record it once.
147
+
148
+ ## Taken over
149
+
150
+ When the user takes a person over, every open task and draft for them is withdrawn
151
+ and no new task about them appears, in any campaign. There is no hand back. Do not
152
+ contact them again. A draft for them is refused with `taken_over`.