@audienti/cli 0.2.0-exora.1 → 0.2.0-exora.11

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 (88) hide show
  1. package/README.md +30 -6
  2. package/cli/account-reply-operations.mjs +26 -1
  3. package/cli/agents.mjs +74 -14
  4. package/cli/assignment-operations.mjs +12 -12
  5. package/cli/assignment.mjs +6 -6
  6. package/cli/attachment-operations.mjs +9 -0
  7. package/cli/attachments.mjs +28 -0
  8. package/cli/audience-operations.mjs +5 -0
  9. package/cli/audiences.mjs +7 -4
  10. package/cli/audienti.mjs +116 -21
  11. package/cli/audit-operations.mjs +17 -0
  12. package/cli/audit.mjs +39 -0
  13. package/cli/budget-operations.mjs +8 -0
  14. package/cli/budgets.mjs +34 -0
  15. package/cli/campaign-find.mjs +155 -0
  16. package/cli/campaign-operations.mjs +30 -0
  17. package/cli/campaigns.mjs +61 -0
  18. package/cli/cohort-operations.mjs +22 -0
  19. package/cli/cohorts.mjs +45 -0
  20. package/cli/connection-registry-operations.mjs +2 -1
  21. package/cli/content.mjs +1 -0
  22. package/cli/discovery-operations.mjs +4 -3
  23. package/cli/discovery.mjs +3 -2
  24. package/cli/file-operations.mjs +29 -0
  25. package/cli/files.mjs +14 -0
  26. package/cli/guardrails-operations.mjs +12 -0
  27. package/cli/guardrails.mjs +37 -0
  28. package/cli/hand-task-operations.mjs +11 -0
  29. package/cli/hand-tasks.mjs +18 -0
  30. package/cli/json-file.mjs +1 -1
  31. package/cli/kv-operations.mjs +17 -0
  32. package/cli/kv.mjs +58 -0
  33. package/cli/mcp.mjs +3 -3
  34. package/cli/notification-operations.mjs +8 -0
  35. package/cli/offer-operations.mjs +1 -1
  36. package/cli/outbound-operations.mjs +16 -0
  37. package/cli/outbound.mjs +54 -0
  38. package/cli/outcome-operations.mjs +28 -0
  39. package/cli/outcomes.mjs +16 -0
  40. package/cli/owner-account-client.mjs +3 -3
  41. package/cli/playbook-help.mjs +79 -0
  42. package/cli/playbook-operations.mjs +36 -0
  43. package/cli/progress.mjs +21 -7
  44. package/cli/question-operations.mjs +12 -0
  45. package/cli/questions.mjs +32 -0
  46. package/cli/queue-operations.mjs +12 -0
  47. package/cli/queue.mjs +25 -0
  48. package/cli/recipient-operations.mjs +1 -1
  49. package/cli/recipients.mjs +6 -2
  50. package/cli/rooms-operations.mjs +48 -0
  51. package/cli/rooms.mjs +160 -0
  52. package/cli/search-operations.mjs +9 -0
  53. package/cli/secret-operations.mjs +7 -0
  54. package/cli/secrets.mjs +30 -0
  55. package/cli/sender-account-operations.mjs +6 -2
  56. package/cli/sender-accounts.mjs +18 -8
  57. package/cli/skills-mcp.mjs +13 -6
  58. package/cli/skills.mjs +73 -16
  59. package/cli/tools-operations.mjs +18 -10
  60. package/cli/tools.mjs +48 -20
  61. package/cli/usage-operations.mjs +8 -0
  62. package/cli/usage.mjs +41 -0
  63. package/cli/work-execution-operations.mjs +7 -0
  64. package/cli/work-execution.mjs +3 -3
  65. package/cli/work-operations.mjs +7 -5
  66. package/cli/work.mjs +11 -6
  67. package/cli/workspaces.mjs +123 -24
  68. package/package.json +2 -2
  69. package/skills/agent-rooms/SKILL.md +140 -0
  70. package/skills/agent-rooms/references/commands.md +78 -0
  71. package/skills/audienti/SKILL.md +112 -6
  72. package/skills/audienti/references/helpers.md +15 -2
  73. package/skills/audienti/references/profile-workflows.md +6 -4
  74. package/skills/audienti/scripts/contracts.mjs +1 -1
  75. package/skills/getting-started/SKILL.md +227 -0
  76. package/skills/getting-started/references/audience.md +143 -0
  77. package/skills/getting-started/references/campaign-draft.md +80 -0
  78. package/skills/getting-started/references/capabilities.md +238 -0
  79. package/skills/getting-started/references/mailbox-check.md +64 -0
  80. package/skills/getting-started/references/outcome-checks.md +125 -0
  81. package/skills/getting-started/references/replies.md +186 -0
  82. package/skills/getting-started/references/reply-checks.md +54 -0
  83. package/skills/playbook-design/SKILL.md +136 -0
  84. package/skills/playbook-design/references/fields.md +146 -0
  85. package/source-manifest.json +86 -39
  86. package/cli/approach-operations.mjs +0 -12
  87. package/cli/experiment-operations.mjs +0 -25
  88. package/cli/experiments.mjs +0 -26
@@ -0,0 +1,238 @@
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
+ "plan": "premium_business",
76
+ "inmail": { "available": true, "quotaPerMonth": 15, "creditsLeft": 11 },
77
+ "limits": { "connectionRequestsPerDay": 20, "connectionRequestsPerWeek": 100, "messagesPerDay": 50, "inmailPerMonth": 15 }
78
+ }
79
+ ]
80
+ }
81
+ ```
82
+
83
+ - `account`: the same LinkedIn account you name in your LinkedIn rows.
84
+ - `checkEverySeconds`: how often you can check this LinkedIn account's sent invitations, at least
85
+ 3600 (one hour). Audienti schedules outcome checks at this cadence; every 6 hours
86
+ when you leave it out. The user never changes it.
87
+ - `plan`: the LinkedIn plan the account has, as the LinkedIn interface shows it: `free`,
88
+ `premium_career`, `premium_business`, `premium_all_in_one`, `sales_navigator_core`,
89
+ `sales_navigator_advanced` or `recruiter_lite`. Leave it out when you cannot tell; never guess.
90
+ The plan sets the InMail allowance. Audienti keeps connection notes to 200 characters for
91
+ now, whatever the plan.
92
+ - `inmail`: whether the LinkedIn account can send InMail, its monthly quota, and `creditsLeft`,
93
+ the balance the interface shows now (Sales Navigator shows it in the compose window and on
94
+ Settings). With a balance, Audienti plans an InMail only while `creditsLeft` is above 0.
95
+ Report `profile.inmail.send` as well; a LinkedIn account counts as InMail-capable only with both.
96
+ - `limits`: the safe limits for this LinkedIn account. Audienti never plans more LinkedIn sends on
97
+ it than its per-day and per-week limits. The user can lower a limit, never
98
+ raise it above the one you report.
99
+
100
+ Report `profile.invitations.read` on the LinkedIn account too, so outcome checks can be assigned to
101
+ you.
102
+
103
+ ## Rules
104
+
105
+ - **Report, never connect.** List only what you can call now. Do not install or sign in
106
+ to anything to fill the list.
107
+ - **Never invent.** Leave out anything you cannot call. A wrong row sends tasks to you
108
+ that you cannot do.
109
+ - **Never report `via: audienti`.** Audienti adds rows for its own hosted tools itself
110
+ when the workspace has credits; a report that sends `audienti` is refused. Leave out
111
+ Audienti's hosted tools unless you reach them through another connector.
112
+ - **Never include secrets** (tokens, cookies, keys) or personal data about the user's
113
+ contacts.
114
+ - Report again when your tools change, and at least every seven days. The user sees
115
+ older rows as stale.
116
+
117
+ ## The catalog
118
+
119
+ Launch rows can be required by queue tasks. Later rows are accepted and shown to the
120
+ user, but no task requires them yet.
121
+
122
+ ### People and companies
123
+
124
+ | Capability | Providers | Access | What it does | Launch |
125
+ | --- | --- | --- | --- | --- |
126
+ | `people.search` | any | read | Find people from audience filters or a question | yes |
127
+ | `people.discover` | any | read | Find look-alikes from a seed person or company | yes |
128
+ | `person.profiles.discover` | any | read | Expand one person into their other profiles | yes |
129
+ | `companies.search` | any | read | Find companies from filters | yes |
130
+ | `company.enrich` | any | read | Fill size, industry, location, site | yes |
131
+ | `company.posts.list` | linkedin, x, instagram, tiktok | read | What a company page has posted | yes |
132
+
133
+ ### Profile: research
134
+
135
+ | Capability | Providers | Access | What it does | Launch |
136
+ | --- | --- | --- | --- | --- |
137
+ | `profile.read` | linkedin, x, instagram, tiktok, github, web | read | Fetch one profile by URL or handle | yes |
138
+ | `profile.enrich` | linkedin, email | read | Fill title, company, location, history from one handle | yes |
139
+ | `profile.find` | email, phone | read | Find a person's handle on this provider | yes |
140
+ | `profile.verify` | email | read | Check the handle is live and deliverable | yes |
141
+ | `profile.posts.list` | linkedin, x, instagram, tiktok | read | What this profile has posted | yes |
142
+ | `profile.comments.list` | linkedin, x, instagram, tiktok | read | Comments this profile wrote on others' posts | yes |
143
+ | `profile.reactions.list` | linkedin, x, instagram, tiktok | read | Reactions this profile gave | yes |
144
+
145
+ ### Profile: as the user (needs `account`)
146
+
147
+ | Capability | Providers | Access | What it does | Launch |
148
+ | --- | --- | --- | --- | --- |
149
+ | `profile.message.send` | email, linkedin, whatsapp, phone, x, instagram | send | Send an approved message | email, linkedin |
150
+ | `profile.inbox.read` | email, linkedin, whatsapp | read | Watch the user's inbox for replies | email, linkedin |
151
+ | `profile.invitations.read` | linkedin | read | Read the user's sent invitations, to see which were accepted or closed | yes |
152
+ | `profile.invitations.received` | linkedin | read | Read the invitations people sent to the user (as the user) | linkedin |
153
+ | `profile.inmail.send` | linkedin | send | Send an approved InMail | yes |
154
+ | `profile.connection.request` | linkedin | send | Send an approved connection request | yes |
155
+ | `profile.connection.withdraw` | linkedin | send | Withdraw a pending request | yes |
156
+ | `profile.connection.remove` | linkedin | send | Remove a connection | later |
157
+ | `profile.follow` | linkedin, x, instagram, tiktok | send | Follow a person or company | linkedin |
158
+ | `profile.unfollow` | linkedin, x, instagram, tiktok | send | Unfollow | linkedin |
159
+ | `profile.mute` | x, instagram | send | Mute | later |
160
+ | `profile.block` | x, instagram, linkedin | send | Block | later |
161
+ | `profile.call` | phone | send | Call a prospect | later |
162
+
163
+ ### Posts and topics
164
+
165
+ | Capability | Providers | Access | What it does | Launch |
166
+ | --- | --- | --- | --- | --- |
167
+ | `topic.track` | any | read | Track a topic for signals | yes |
168
+ | `posts.search` | linkedin, x, instagram, tiktok, web | read | Find posts about a topic | yes |
169
+ | `post.read` | linkedin, x, instagram, tiktok | read | Fetch one post with its type | yes |
170
+ | `post.comments.list` | linkedin, x, instagram, tiktok | read | Who commented, and what they said | yes |
171
+ | `post.reactions.list` | linkedin, x, instagram, tiktok | read | Who reacted | yes |
172
+ | `post.comment` | linkedin, x, instagram, tiktok | send | Comment as the user (as the user) | linkedin |
173
+ | `post.react` | linkedin, x, instagram, tiktok | send | React as the user (as the user) | linkedin |
174
+ | `post.publish` | linkedin, x, instagram, tiktok | send | Publish a post as the user (as the user) | linkedin |
175
+ | `profile.visit` | linkedin | read | Visit a profile as a person would (as the user) | linkedin |
176
+ | `profile.views.read` | linkedin | read | Read who viewed the user's profile (as the user) | linkedin |
177
+
178
+ ### Web, calendar, records, owner, integration
179
+
180
+ | Capability | Providers | Access | What it does | Launch |
181
+ | --- | --- | --- | --- | --- |
182
+ | `web.search` | web | read | Search the web | yes |
183
+ | `web.page.read` | web | read | Read one page | yes |
184
+ | `calendar.read` | google, microsoft | read | Free and busy time | later |
185
+ | `calendar.freebusy.read` | google, microsoft | read | Read the user's free and busy times to offer slots in a reply | later |
186
+ | `calendar.event.create` | google, microsoft | write | Create an event and send the invite from the user's calendar | later |
187
+ | `meeting.book` | google, microsoft | write | Book or propose a meeting | later |
188
+ | `meeting.reschedule` | google, microsoft | write | Move a booked meeting | later |
189
+ | `meeting.cancel` | google, microsoft | write | Cancel a booked meeting | later |
190
+ | `crm.records.search` | hubspot, salesforce, pipedrive, other | read | Find records in the user's CRM | yes |
191
+ | `crm.record.read` | hubspot, salesforce, pipedrive, other | read | Read one CRM record | yes |
192
+ | `crm.record.write` | hubspot, salesforce, pipedrive, other | write | Write back to one CRM record | yes |
193
+ | `owner.notify` | push, slack, sms, email | send | Reach the user on their own channel | yes |
194
+ | `handtask.prepare` | any | write | Prepare a step a person does by hand: script, note text, outline, gift choice | yes |
195
+ | `webhook.receive` | none | read | Take a push instead of polling | later |
196
+ | `webhook.send` | none | send | Post events to the user's URL | later |
197
+
198
+ `owner.notify` means you can reach your user outside Audienti: chat, push, Slack, SMS or email. Report it
199
+ and Audienti puts a **Tell your user about an alert** task in your queue for each alert it records in
200
+ the workspace; the task carries the message and a link, and you mark it done once you delivered it. An
201
+ agent that has not reported `owner.notify` gets no such task.
202
+
203
+ ### Tools you use for yourself
204
+
205
+ | Capability | Providers | Access | What it does | Launch |
206
+ | --- | --- | --- | --- | --- |
207
+ | `tool.media.transcribe` | none | read | Video or audio to text | later |
208
+ | `tool.image.read` | none | read | Read text and content out of an image | later |
209
+ | `tool.proxy.session.start` | none | write | Sticky residential session for your own browser, metered by bandwidth | yes |
210
+ | `tool.proxy.session.rotate` | none | write | New IP on the current session | yes |
211
+ | `tool.proxy.config.read` | none | read | Read region, stickiness and bandwidth cap | yes |
212
+ | `tool.proxy.config.write` | none | write | Set region, stickiness and bandwidth cap | yes |
213
+
214
+ Your runtime's documents, spreadsheets, files, code, memory, tasks and chat are not
215
+ capabilities. Leave them out.
216
+
217
+ ## Older reports
218
+
219
+ `audienti agent inventory report` (categories such as `web_search` or `crm`) and the
220
+ per-account `audienti agent capabilities report` with `provider` and `accountRef` are
221
+ still accepted for one release. Audienti translates both into rows. Send the format above
222
+ instead.
223
+
224
+ Translated inventory reports match fewer queue tasks than before:
225
+
226
+ - `company_data` alone matches none of the find, verify or enrich tasks.
227
+ - `linkedin` alone no longer matches find or enrich tasks.
228
+ - `web_search` alone no longer matches enrich tasks.
229
+ - `people_data` alone no longer matches verify tasks.
230
+ - `email` and `linkedin` match send tasks only for their own provider and only with
231
+ `send` access; `social` matches no send task.
232
+
233
+ A LinkedIn message can carry a file. Upload it first with `audienti attachments upload FILE` (an image up to 5 MB,
234
+ or a voice note up to 5 MB and 2 minutes; Audienti reads a WAV, M4A, OGG or MP3 voice note's length from the file; add `--duration SECONDS` only for a file it cannot read). Name the returned id in
235
+ `content.attachments` of the `linkedin.send` action, and tag that action `profile.message.send`. A connection
236
+ request carries no file. A voice note goes only to a connection. When you send from your own account, run
237
+ `audienti attachments get ID --out PATH` to fetch the file, then attach it in LinkedIn. A managed account sends it
238
+ for you.
@@ -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,125 @@
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
+ A `steps.run` task (a comment, react, withdraw, follow or unfollow step of a playbook, for one person) takes these
66
+ results instead. The task names the step in `detail.step` and tells you how to find the post.
67
+
68
+ - `linkedin.comment {person, postUrl}`: you commented on the post. Comment only when you can add
69
+ material value; otherwise react.
70
+ - `linkedin.reaction {person, postUrl, reaction}`: you reacted. `reaction` is `like` (the default),
71
+ `celebrate`, `support`, `love`, `insightful` or `funny`.
72
+ - `linkedin.follow {person}` and `linkedin.unfollow {person}`: for a follow or unfollow step. Audienti records
73
+ that your account now follows the person, or no longer does. It skips a later Follow step for someone you
74
+ already follow, and an Unfollow step for someone you do not.
75
+ - `linkedin.step_skipped {person, reason}`: nothing to engage with, or your user declined.
76
+ - `linkedin.invite_closed {person, reason: withdrawn}`: for a withdraw step.
77
+
78
+ Audienti records the step as run, and the playbook goes on to its next step.
79
+
80
+ A `handtasks.prepare` task (a Custom step of a playbook: something you cannot do, such as a phone call, a voicemail, a
81
+ handwritten note, a video or a gift) asks you to prepare the step for a person who will do it by hand. The task names the
82
+ step in `detail.step`. You do not contact the person. Write everything the person needs yourself, from the step's text, with
83
+ no fixed template, and report it.
84
+
85
+ - `handtask.prepared {person, items}`: `items` is 1 to 12 pieces of `{label, text}`, such as a call script, a voicemail
86
+ script, the text of a note, a video outline, or a gift choice and its message. Add how to reach the person (a
87
+ phone number if you have one) as an item. Audienti puts them in the user's Hand
88
+ tasks list. The person does the step, records what happened and marks it done, and the playbook goes on. If you report
89
+ again while the person has not started, the new material replaces the old.
90
+
91
+ An `invitations.receive` task (every 6 hours, only on a seat whose connector can read received invitations)
92
+ asks you to read the connection requests people sent to your user. Read only the pending received list. Never
93
+ accept, decline or open anyone. Report each pending request:
94
+
95
+ - `linkedin.invitation_received {ref, profileUrl, name, sentAt}`: give `ref` (the request id your connector shows,
96
+ otherwise the person's profile address) or `profileUrl`; `name` and `sentAt` when you have them. Audienti records
97
+ each request once and tells your user once. Report the same request again and it returns `already_recorded`.
98
+
99
+ Report done with no results when no request is pending.
100
+
101
+ Leave out anyone still pending. They stay on the next check until they accept or the
102
+ wait times out.
103
+
104
+ ## What the report returns
105
+
106
+ `results` has one entry per result you sent, in the same order. Each person is recorded
107
+ on their own, so one person who cannot be recorded does not hold back the others.
108
+
109
+ - `status`: `recorded` (written now), `already_recorded` (an earlier report wrote it;
110
+ `eventId` is that earlier event) or `not_recorded` (nothing was written).
111
+ - `reason`: why, when there is one. `participation_ended` or
112
+ `participation_unavailable` means the fact was recorded but nobody moves, because
113
+ the person's campaign participation ended or its playbook cannot be read.
114
+ `not_found` with `not_recorded` means the person is no longer in the workspace.
115
+ - `next`: the person's next step after the report, when there is one.
116
+ - `failed`: the accept was recorded, but planning the next step failed. Audienti shows
117
+ the failure on the person's timeline; `reason` holds its code. Sending the same
118
+ accept again returns the same failure until it is resolved.
119
+
120
+ ## Errors
121
+
122
+ - `not_claimed`: claim the task first, or your lease expired. Read the queue again.
123
+ - `not_on_list`: you reported a person the task does not list. Report only the listed
124
+ people.
125
+ - `validation_error`: a result has a wrong kind, person id or time.
@@ -0,0 +1,186 @@
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`, `referral` or `wrong_person` (the prospect is the wrong person and
11
+ names no one else). A stop, a bounce or a referral must carry its typed
12
+ `result`, or the report is refused; see [Stop, bounce and referral](#stop-bounce-and-referral).
13
+ 3. Draft the answer from the mailbox or LinkedIn account the reply came to (the assignee's).
14
+ - Bind the recipient and the sender, or the answer waits in Blocked: the recipient's
15
+ `contactId` comes from `audienti prospects contacts list PERSON`, and the
16
+ `senderAccountId` is the user's confirmed sender account for that mailbox or LinkedIn account
17
+ (`audienti sender accounts list`).
18
+ - If you report `calendar.freebusy.read`, read the user's free times and offer up
19
+ to six slots in the answer. Put the same times in `slots`, so the user sees them on
20
+ the card: "Slots from your calendar · Thu 10:00, 14:30 ET". A scheduling tool that
21
+ reports only `calendar.read` is not the user's calendar and offers no slots.
22
+ - Without a calendar, ask the prospect for a time that suits them, and leave `slots`
23
+ out. The user's card says no agent has a calendar and links to the Capabilities
24
+ screen.
25
+ 4. Record the classification and the draft in one result, keeping the reply open:
26
+
27
+ ```sh
28
+ audienti prospects replies set PERSON --workspace ID --evidence EVIDENCE --file RESULT.json
29
+ ```
30
+
31
+ ```json
32
+ {
33
+ "requestId": "a new UUID for each result",
34
+ "expectedVersion": 0,
35
+ "status": "needs_handling",
36
+ "outcome": null,
37
+ "reason": "Interested and asked for a time on Thursday.",
38
+ "responseEvidenceId": null,
39
+ "classification": "interested",
40
+ "draft": {
41
+ "type": "email.send",
42
+ "content": { "subject": "Re: Late invoices", "body": "Thursday works. I have 10:00 or 14:30 ET. Which suits you?" },
43
+ "recipient": { "prospectId": "PERSON", "email": "jane@example.com", "contactId": "CONTACT" },
44
+ "sender": { "provider": "email", "accountRef": "owner@example.com", "senderAccountId": "SENDER" },
45
+ "reason": "Offers the two free slots on Thursday the prospect asked for.",
46
+ "slots": [
47
+ { "start": "2026-11-05T15:00:00Z", "end": "2026-11-05T15:30:00Z", "timeZone": "America/New_York" },
48
+ { "start": "2026-11-05T19:30:00Z", "end": "2026-11-05T20:00:00Z", "timeZone": "America/New_York" }
49
+ ]
50
+ }
51
+ }
52
+ ```
53
+
54
+ `expectedVersion` is the reply's current version (`audienti prospects replies get`).
55
+ The draft becomes a revision the user reviews in Operator. Nothing is sent until the
56
+ user approves it. A second result for the same reply replaces a draft the user has
57
+ not decided yet.
58
+ 5. Report the task done.
59
+
60
+ The owner can also close a reply in Operator with "Mark handled" or "Not a reply". That records the same
61
+ state as `replies set` with status `handled` or `not_reply`, and withdraws any answer you drafted for it
62
+ that no agent has claimed to send. Read `replies get` before you set a reply again.
63
+
64
+ To close a reply for the owner by CLI, pass the version you read as `expectedVersion`. Handled with no
65
+ answer is `{"requestId":"<uuid>","expectedVersion":1,"status":"handled","outcome":"closed_without_response","reason":"The owner answered by phone."}`.
66
+ Not a reply is `{"requestId":"<uuid>","expectedVersion":1,"status":"not_reply","reason":"An automatic message."}`;
67
+ it has no outcome. Give `--file` an absolute path.
68
+
69
+ The classification records a `replied` event on the person's active campaigns, except
70
+ for `bounce` and `out_of_office`, which are not replies from the person.
71
+ An `out_of_office` reply records an auto-reply instead. The playbook's out-of-office rule
72
+ snoozes the person: the wait and the next step hold for 7 days, or until the return date
73
+ when you pass `returnsOn`. Nothing is sent meanwhile and the person does not move. A
74
+ `wrong_person` reply that names no one stops the sends and leaves the reply for the user to
75
+ settle: ask who the right person is. When the reply names the right person, classify it as a
76
+ `referral` with its typed `result`.
77
+
78
+ ## Stop, bounce and referral
79
+
80
+ These three need no decision from the user. Classify the reply, report the typed
81
+ `result` next to the classification it matches, and Audienti records it against the
82
+ person. The classification without its result is refused with 400 naming the result it
83
+ needs (`reply.stop`, `email.bounce` or `reply.referral`). Use the prospect's words,
84
+ verbatim, for `quote`.
85
+
86
+ - **Stop** (`stop`): the prospect asked not to be contacted. Report status `handled`,
87
+ outcome `suppressed` and no draft:
88
+
89
+ ```json
90
+ { "classification": "stop", "status": "handled", "outcome": "suppressed",
91
+ "result": { "type": "reply.stop", "scope": "person", "quote": "Please remove me from your list." } }
92
+ ```
93
+
94
+ The person is suppressed for good on every channel, their pending sends and open
95
+ tasks are withdrawn, and they leave their campaigns. The user cannot override it.
96
+ Use `"scope": "company"` only when they asked to stop for their whole company:
97
+ everyone at the company's domain is suppressed, and so is anyone added later. The
98
+ domain goes on the do-not-contact list marked "Asked to stop", and no one can remove it.
99
+ Never contact a person who asked to stop again, on any channel.
100
+ - **Bounce** (`bounce`): a delivery failure came back instead of a reply. Report
101
+ status `not_reply` and no draft:
102
+
103
+ ```json
104
+ { "classification": "bounce", "status": "not_reply", "outcome": null,
105
+ "result": { "type": "email.bounce", "address": "priya@example.com", "kind": "hard", "evidence": "550 5.1.1 User unknown" } }
106
+ ```
107
+
108
+ `address` is the address that bounced: the person's own address, or one nobody in the
109
+ workspace holds. An address another person holds is refused with 409
110
+ `address_belongs_to_another_person`; report the bounce on the reply that came back
111
+ about that person instead.
112
+ `hard` means the address does not exist or refuses mail: it is marked bounced and
113
+ blocked in every campaign, and the person leaves the campaign as Bounced. `soft`
114
+ means a full mailbox or a temporary failure: the address is held for 24 hours, then
115
+ a retry task asks you to send the message once more. Retried once means exactly one
116
+ send: after the 24 hours the address takes one send, and every other send to it is
117
+ refused with `recipient_contact_bounced` until that send's outcome is known. A second
118
+ bounce within 14 days of the retry counts as hard; with no bounce in that time the
119
+ address opens again. Put the bounce message and its status code in `evidence`.
120
+ - **Referral** (`referral`): the prospect is the wrong person and named someone else.
121
+ Report status `needs_handling` with a thank-you as the draft, and name the person:
122
+
123
+ ```json
124
+ { "classification": "referral", "status": "needs_handling", "outcome": null,
125
+ "result": { "type": "reply.referral", "name": "Sam Lee", "title": "Finance lead",
126
+ "email": "sam@example.com", "quote": "Wrong person, try Sam Lee in finance." },
127
+ "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" } }
128
+ ```
129
+
130
+ Give `linkedinUrl` or `email` when the prospect gave them. Audienti finds or adds the
131
+ named person (an email someone in the workspace already holds names that person), links
132
+ the two records, and leaves the referrer as Not a fit with the note "referred to Sam
133
+ Lee". When the email and the LinkedIn profile belong to different people, the referral
134
+ is refused with 409 `identity_conflict`. Reporting the same referral again, from
135
+ another reply, changes nothing. The named person is not in the queue yet: a
136
+ `prospects.verify` task asks you to check them against the campaign's audience
137
+ first. The thank-you is an answer like any other: the user approves it in Operator
138
+ before it is sent.
139
+
140
+ Every first email you draft ends with one plain sentence that lets the prospect opt
141
+ out, such as "If this is not for you, reply and I will not write again." When they do,
142
+ report it as a stop.
143
+
144
+ ## After the user approves
145
+
146
+ A `work.execute` task with `action: send_reply` follows. Claim it, send the answer in
147
+ the thread the prospect wrote in, report the result, then close the reply with status
148
+ `handled`, outcome `response_recorded` and the sent message's evidence as
149
+ `responseEvidenceId`.
150
+
151
+ ## When a meeting is booked
152
+
153
+ - The prospect picks a slot: send the invite from the user's calendar
154
+ (`calendar.event.create`), then record `"meeting": { "at": "…Z", "source": "sent_invite" }`
155
+ in a `replies set` result.
156
+ - The prospect sent an invite: set `"invite": true` with the classification. A
157
+ `meetings.confirm` task follows, assigned to one agent of the person's assignee that
158
+ is allowed to make workspace changes and reports `calendar.event.create` or
159
+ `meeting.book`. It accepts the invite on the
160
+ assignee's calendar and records `"source": "received_invite"`. If the assignee has no
161
+ such agent, no task appears.
162
+ - The meeting moves the person to the playbook's goal stage. Record it once.
163
+
164
+ ## Before a meeting is booked
165
+
166
+ Record where an unbooked meeting stands with `"meetingState"` in a `replies set` result:
167
+
168
+ - `requested`: a time was asked for or offered.
169
+ - `accepted`: they agreed, and no time is fixed yet.
170
+ - `declined`: they said no to the meeting. A plain no is not a request to stop; classify the
171
+ reply on its own.
172
+
173
+ Each state is written once, and only when it differs from the last one. Nothing is written after
174
+ a booking. If a playbook step waits for a meeting and the wait runs out while the request is
175
+ `requested` or `accepted`, Audienti first asks you for a follow-up, up to 3 times (the playbook
176
+ can change the count and the wait; the default wait is 5 days). Write each follow-up yourself
177
+ after you read the whole conversation, and send it on the thread's channel. Do not use a fixed
178
+ template unless the user put one in your instructions. Save it as a plan action with the
179
+ `playbookStepKey` the task names. After the last wait runs out, Audienti records
180
+ `meeting_no_answer` itself and tells the owner. Do not record it.
181
+
182
+ ## Taken over
183
+
184
+ When the user takes a person over, every open task and draft for them is withdrawn
185
+ and no new task about them appears, in any campaign. There is no hand back. Do not
186
+ contact them again. A draft for them is refused with `taken_over`.