@audienti/cli 0.2.0-exora.3 → 0.2.0-exora.30
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.
- package/README.md +2 -2
- package/cli/account-replies.mjs +16 -2
- package/cli/account-reply-operations.mjs +28 -1
- package/cli/agents.mjs +56 -14
- package/cli/attachment-operations.mjs +9 -0
- package/cli/attachments.mjs +28 -0
- package/cli/audience-operations.mjs +1 -1
- package/cli/audienti.mjs +118 -27
- package/cli/audit-operations.mjs +2 -2
- package/cli/campaign-find.mjs +155 -0
- package/cli/campaign-operations.mjs +8 -4
- package/cli/campaigns.mjs +38 -3
- package/cli/cohort-operations.mjs +1 -1
- package/cli/content.mjs +2 -0
- package/cli/decision-operations.mjs +35 -0
- package/cli/decision.mjs +51 -0
- package/cli/discovery-operations.mjs +2 -1
- package/cli/discovery.mjs +3 -2
- package/cli/duplicate-operations.mjs +22 -0
- package/cli/duplicates.mjs +22 -0
- package/cli/hand-task-operations.mjs +11 -0
- package/cli/hand-tasks.mjs +18 -0
- package/cli/intent-operations.mjs +44 -0
- package/cli/intent.mjs +109 -0
- package/cli/json-file.mjs +1 -1
- package/cli/kv-operations.mjs +17 -0
- package/cli/kv.mjs +58 -0
- package/cli/mcp.mjs +4 -4
- package/cli/outcome-operations.mjs +28 -0
- package/cli/outcomes.mjs +16 -0
- package/cli/owner-account-client.mjs +5 -4
- package/cli/playbook-help.mjs +87 -0
- package/cli/playbook-operations.mjs +40 -0
- package/cli/progress.mjs +19 -8
- package/cli/prospecting-settings-operations.mjs +1 -1
- package/cli/question-operations.mjs +4 -4
- package/cli/questions.mjs +4 -4
- package/cli/queue-operations.mjs +7 -6
- package/cli/queue.mjs +4 -3
- package/cli/recipient-operations.mjs +1 -1
- package/cli/recipients.mjs +6 -2
- package/cli/rooms-operations.mjs +26 -5
- package/cli/rooms.mjs +32 -6
- package/cli/secret-operations.mjs +7 -0
- package/cli/secrets.mjs +30 -0
- package/cli/sender-account-operations.mjs +8 -3
- package/cli/sender-accounts.mjs +20 -10
- package/cli/skills-mcp.mjs +6 -2
- package/cli/skills.mjs +55 -12
- package/cli/tools-operations.mjs +19 -10
- package/cli/tools.mjs +48 -20
- package/cli/work-execution-operations.mjs +7 -0
- package/cli/work-execution.mjs +3 -3
- package/cli/work-operations.mjs +5 -4
- package/cli/work.mjs +2 -2
- package/cli/workspaces.mjs +104 -22
- package/package.json +2 -2
- package/skills/agent-rooms/SKILL.md +140 -0
- package/skills/agent-rooms/references/commands.md +78 -0
- package/skills/audienti/SKILL.md +111 -5
- package/skills/audienti/references/helpers.md +15 -2
- package/skills/audienti/references/profile-workflows.md +4 -2
- package/skills/audienti/scripts/contracts.mjs +1 -1
- package/skills/getting-started/SKILL.md +188 -154
- package/skills/getting-started/references/audience.md +72 -7
- package/skills/getting-started/references/campaign-draft.md +56 -13
- package/skills/getting-started/references/capabilities.md +238 -0
- package/skills/getting-started/references/mailbox-check.md +64 -0
- package/skills/getting-started/references/outcome-checks.md +125 -0
- package/skills/getting-started/references/replies.md +314 -0
- package/skills/getting-started/references/reply-checks.md +54 -0
- package/skills/playbook-design/SKILL.md +139 -0
- package/skills/playbook-design/references/fields.md +145 -0
- package/source-manifest.json +74 -48
- package/cli/approach-operations.mjs +0 -24
- package/skills/getting-started/references/inventory.md +0 -52
|
@@ -0,0 +1,314 @@
|
|
|
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`, `not_interested` (a clear no thanks),
|
|
10
|
+
`out_of_office`, `stop`, `bounce`, `referral`, `wrong_person` (the prospect is the wrong person and
|
|
11
|
+
names no one else) or `unclear` (you cannot tell what they mean; it holds the person for a decision, see below). 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
|
+
## Label a message with its words
|
|
79
|
+
|
|
80
|
+
A label says what a message means. Read the names with `audienti intent list`. A label goes on a message
|
|
81
|
+
that is stored as evidence. Get its `EVIDENCE` ID from `audienti prospects conversation append` (when you
|
|
82
|
+
store the message yourself) or `audienti prospects conversation list PERSON` (for one already stored); intake
|
|
83
|
+
returns it too. Record one to three labels, each with the `quote` that supports it (at most 300 characters,
|
|
84
|
+
copied from the message) and a plain `reason` (at most 200 characters, no IDs or error codes):
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
audienti intent record --person PERSON --evidence EVIDENCE \
|
|
88
|
+
--label interested --quote "we are interested in a demo" --reason "They ask for a demo next week."
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Flags carry one label. Use `--file LABELS.json` for several labels or a typed detail (the `result` of a
|
|
92
|
+
stop, bounce or referral; `meeting` for `meeting_booked`; `returnsOn` for `out_of_office`). A stop that
|
|
93
|
+
asks for everything to end looks like this:
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
[{"label": "stop", "quote": "please stop writing to me", "reason": "They ask us to stop.",
|
|
97
|
+
"result": {"type": "reply.stop", "scope": "person"}}]
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`audienti intent record --help` prints this example. Give the same `--request-id` to repeat a call safely. Add
|
|
101
|
+
`confidence` (`high`, `medium` or `low`) when you are unsure. A low-confidence label does not fire its rule:
|
|
102
|
+
the person is held and a decision opens for your user to pick from (see below). `stop` and `bounce` always
|
|
103
|
+
act, whatever the confidence. A message keeps the labels it has: a second request that repeats
|
|
104
|
+
one is refused with `already_recorded`, and one that would give the message a second reading is refused.
|
|
105
|
+
|
|
106
|
+
Audienti checks every label and writes nothing if one fails. A refusal names the `field`, the `allowed`
|
|
107
|
+
values and the `next` step. The checks:
|
|
108
|
+
|
|
109
|
+
- The message is stored with its text. A message stored without a body cannot carry a label: submit the text as evidence.
|
|
110
|
+
- The name is on the list, and fits the message direction and channel (`bounce` is email only).
|
|
111
|
+
- The quote is in the message (subject and body), ignoring case and spacing.
|
|
112
|
+
- The reason is plain words a person can read.
|
|
113
|
+
- The labels fit together: `stop`, `bounce` and `out_of_office` stand alone; a message has one reading of the
|
|
114
|
+
reply and at most one meeting label.
|
|
115
|
+
- The typed detail is present: a `stop`, `bounce` or `referral` needs its `result`; `meeting_booked` needs `meeting`.
|
|
116
|
+
|
|
117
|
+
A recorded label shows to the user as a chip with the quote, the reason, how sure you were and who recorded it, on the
|
|
118
|
+
reply card in Operator, the person's page and the Pipeline card. Write the `quote` and `reason` for a person to read. The same
|
|
119
|
+
labels come back as `labels` on `audienti prospects timeline PERSON`, `audienti prospects replies get PERSON` (the latest reply, or one message with `--evidence`) and `audienti pipeline show`. A label event on the timeline carries the label once, under `event.label`. Mark a guess with `confidence: low`: the chip says so.
|
|
120
|
+
|
|
121
|
+
To get a second reading before you record, `audienti intent suggest --channel email --text "..."` asks a model
|
|
122
|
+
to suggest labels. It costs credits: the call prints the price per million input tokens first, and a message
|
|
123
|
+
uses a small part of that. It returns up to two suggestions, each with a `quote`, a `reason`, a `confidence` and
|
|
124
|
+
`needs` (the typed detail to add, for example the `scope` of a stop). It records nothing. Keep, change or drop
|
|
125
|
+
a suggestion, then record it with `intent record`, which runs every check above again.
|
|
126
|
+
|
|
127
|
+
`replies set` records the same labels. Add `"intent": {"quote": "...", "reason": "..."}` next to `classification`
|
|
128
|
+
or `meetingState` and it runs the same checks and stores the words on the event. Without `intent` it works as before.
|
|
129
|
+
|
|
130
|
+
## A label your playbook defines
|
|
131
|
+
|
|
132
|
+
A playbook may declare labels of its own, such as "asked about pricing". See them with `audienti intent list --playbook PLAYBOOK_ID`: each shows its name
|
|
133
|
+
(`custom.asked_pricing`), what it means and whether a rule uses it. Record one like any other label:
|
|
134
|
+
`audienti intent record --person PERSON --evidence EVIDENCE --label custom.asked_pricing --quote "..." --reason "..."`. The same checks run, and
|
|
135
|
+
the label fits only the people whose running playbook declares it. If a rule names the label, the rule acts. If none does, the person is held and a
|
|
136
|
+
decision opens for your user, with the trigger `custom_label_no_rule`. A custom label is never a stop or a bounce, and `replies set` takes core labels only.
|
|
137
|
+
|
|
138
|
+
## A label is wrong
|
|
139
|
+
|
|
140
|
+
A person corrects a label; an agent cannot. If you see a wrong label, tell your user and say which one. They change it
|
|
141
|
+
with the "Change label" menu on the reply card or the person's page, or with `audienti intent correct --person PERSON
|
|
142
|
+
--event EVENT --label NAME --quote "..." --reason "..."`. `--retract` withdraws it. The `EVENT` is the `id` the label
|
|
143
|
+
shows. Nothing is edited: the old label stays in the history, and the labels in force are the ones no correction replaced.
|
|
144
|
+
The new label passes the same checks as a first recording. If it would send the person somewhere else, nothing moves:
|
|
145
|
+
Operator asks "Move back to ..." and the person picks. A change away from a stop never lifts the stop. A decision
|
|
146
|
+
asks "Lift the stop on this person?", and the stop stays until a person picks to lift it. An agent token gets `human_only`.
|
|
147
|
+
|
|
148
|
+
## Label a message you sent
|
|
149
|
+
|
|
150
|
+
A message you sent can carry a label too. Today one fits: `meeting_requested`, for a message that asks for a
|
|
151
|
+
meeting or offers a time ("could we find 20 minutes next week?"). Without it Audienti does not know a meeting
|
|
152
|
+
was asked for, so the playbook's meeting wait cannot follow it up. With it the timeline shows the request, the
|
|
153
|
+
wait sends up to 3 follow-ups, and a request nobody answers is marked No answer. No reply is needed.
|
|
154
|
+
|
|
155
|
+
Do it after the send, in two calls. Store what you sent, then label it:
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
audienti prospects conversation append PERSON --workspace ID --file ITEM.json
|
|
159
|
+
audienti intent record --person PERSON --evidence EVIDENCE \
|
|
160
|
+
--label meeting_requested --quote "find 20 minutes next week" --reason "We ask for a call next week."
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
`ITEM.json` holds a new `requestId` and the sent message. Give it the text exactly as you sent it:
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
{ "requestId": "<uuid>",
|
|
167
|
+
"item": {
|
|
168
|
+
"source": { "provider": "email", "accountRef": "you@yourcompany.com", "itemId": "<the provider's message ID>", "threadId": null },
|
|
169
|
+
"payload": { "direction": "outbound", "conversationType": "direct", "occurredAt": "2026-11-06T14:00:00Z",
|
|
170
|
+
"subject": "Ten minutes on late invoices?", "body": "Hi Jordan, could we find 20 minutes next week?", "participants": null } } }
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The call returns `EVIDENCE`. If you stored the message already, to report the send (`account_evidence`) or as
|
|
174
|
+
the `responseEvidenceId` of an answer, use that same ID. The `quote` must be words from what you sent.
|
|
175
|
+
The same checks apply: a message with no stored text, a quote that is not in it, or a label that does not fit a sent message (`interested` fits a reply, not an ask) is refused and writes nothing.
|
|
176
|
+
|
|
177
|
+
- Label only a message that asks for or offers a meeting. A message that does not is not labeled.
|
|
178
|
+
- A follow-up that asks again needs no label: the request is already on the timeline, so a repeat writes nothing.
|
|
179
|
+
The same message cannot take the label twice (`already_recorded`).
|
|
180
|
+
- The label goes on the person's running campaign. A person runs in one campaign per workspace, so you need no option; `--participation ID` names the run if you want it explicit. The answer to `work execution send` or `report` carries that `participationId` when the send started a playbook step and the person still runs in the campaign run the send started, also when you repeat the call. A person who left and rejoined has a new run: a repeat of the old send names none. The label works without it.
|
|
181
|
+
- An answer to a reply that offers times is labeled the same way, with the evidence of the answer you sent.
|
|
182
|
+
|
|
183
|
+
## When the rules do not decide: decisions
|
|
184
|
+
|
|
185
|
+
A reply the rules cannot act on holds the person, and a decision asks your user to pick. It opens when a
|
|
186
|
+
label is `unclear`, when a label has low confidence (not `stop` or `bounce`), when a reply has had no label
|
|
187
|
+
for 24 hours, or when you ask for one with `audienti decision ask`. While it is open nothing is sent to the
|
|
188
|
+
person. A `decisions.prepare` task tells you to add your view:
|
|
189
|
+
|
|
190
|
+
1. Read the options the playbook allows: `audienti decision options DECISION_ID`. Only these ids are valid.
|
|
191
|
+
A stage option names its stage (`stageName`), a snooze option reads "Snooze 30 days, then continue", and each
|
|
192
|
+
trigger says who recorded the event behind it (`recordedBy`: kind and name). The clock raises a trigger with none.
|
|
193
|
+
2. Choose two to five. Write one plain line for each (at most 140 characters, no IDs or error codes) and
|
|
194
|
+
recommend one: `audienti decision prepare DECISION_ID --recommended ID --option ID=ANALYSIS ...`.
|
|
195
|
+
Audienti refuses an option outside the list and names the allowed ids.
|
|
196
|
+
3. Report the task done. Only your user picks, in Operator or with `audienti decision answer` under their own
|
|
197
|
+
sign-in. Audienti never applies your recommendation. After 24 hours without your view the user sees the
|
|
198
|
+
options without analyses. An open decision turns stale after 7 days; the person stays held.
|
|
199
|
+
|
|
200
|
+
A reply you can label clearly needs no decision: record the label with `audienti intent record` and the rules
|
|
201
|
+
act. A label recorded later ends the hold.
|
|
202
|
+
|
|
203
|
+
## Stop, bounce and referral
|
|
204
|
+
|
|
205
|
+
These three need no decision from the user. Classify the reply, report the typed
|
|
206
|
+
`result` next to the classification it matches, and Audienti records it against the
|
|
207
|
+
person. The classification without its result is refused with 400 naming the result it
|
|
208
|
+
needs (`reply.stop`, `email.bounce` or `reply.referral`). Use the prospect's words,
|
|
209
|
+
verbatim, for `quote`.
|
|
210
|
+
|
|
211
|
+
- **Stop** (`stop`): the prospect asked not to be contacted. Report status `handled`,
|
|
212
|
+
outcome `suppressed` and no draft:
|
|
213
|
+
|
|
214
|
+
```json
|
|
215
|
+
{ "classification": "stop", "status": "handled", "outcome": "suppressed",
|
|
216
|
+
"result": { "type": "reply.stop", "scope": "person", "quote": "Please remove me from your list." } }
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
The person is suppressed for good on every channel, their pending sends and open
|
|
220
|
+
tasks are withdrawn, and they leave their campaigns. The user cannot override it.
|
|
221
|
+
Use `"scope": "company"` only when they asked to stop for their whole company:
|
|
222
|
+
everyone at the company's domain is suppressed, and so is anyone added later. The
|
|
223
|
+
domain goes on the do-not-contact list marked "Asked to stop", and no one can remove it.
|
|
224
|
+
Never contact a person who asked to stop again, on any channel.
|
|
225
|
+
- **Bounce** (`bounce`): a delivery failure came back instead of a reply. Report
|
|
226
|
+
status `not_reply` and no draft:
|
|
227
|
+
|
|
228
|
+
```json
|
|
229
|
+
{ "classification": "bounce", "status": "not_reply", "outcome": null,
|
|
230
|
+
"result": { "type": "email.bounce", "address": "priya@example.com", "kind": "hard", "evidence": "550 5.1.1 User unknown" } }
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
`address` is the address that bounced: the person's own address, or one nobody in the
|
|
234
|
+
workspace holds. An address another person holds is refused with 409
|
|
235
|
+
`address_belongs_to_another_person`; report the bounce on the reply that came back
|
|
236
|
+
about that person instead.
|
|
237
|
+
`hard` means the address does not exist or refuses mail: it is marked bounced and
|
|
238
|
+
blocked in every campaign, and the person leaves the campaign as Bounced. `soft`
|
|
239
|
+
means a full mailbox or a temporary failure: the address is held for 24 hours, then
|
|
240
|
+
a retry task asks you to send the message once more. Retried once means exactly one
|
|
241
|
+
send: after the 24 hours the address takes one send, and every other send to it is
|
|
242
|
+
refused with `recipient_contact_bounced` until that send's outcome is known. A second
|
|
243
|
+
bounce within 14 days of the retry counts as hard; with no bounce in that time the
|
|
244
|
+
address opens again. Put the bounce message and its status code in `evidence`.
|
|
245
|
+
- **Referral** (`referral`): the prospect is the wrong person and named someone else.
|
|
246
|
+
Report status `needs_handling` with a thank-you as the draft, and name the person:
|
|
247
|
+
|
|
248
|
+
```json
|
|
249
|
+
{ "classification": "referral", "status": "needs_handling", "outcome": null,
|
|
250
|
+
"result": { "type": "reply.referral", "name": "Sam Lee", "title": "Finance lead",
|
|
251
|
+
"email": "sam@example.com", "quote": "Wrong person, try Sam Lee in finance." },
|
|
252
|
+
"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" } }
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Give `linkedinUrl` or `email` when the prospect gave them. Audienti finds or adds the
|
|
256
|
+
named person (an email someone in the workspace already holds names that person), links
|
|
257
|
+
the two records, and leaves the referrer as Not a fit with the note "referred to Sam
|
|
258
|
+
Lee". When the email and the LinkedIn profile belong to different people, the referral
|
|
259
|
+
is refused with 409 `identity_conflict`. Reporting the same referral again, from
|
|
260
|
+
another reply, changes nothing. The named person is not in the queue yet: a
|
|
261
|
+
`prospects.verify` task asks you to check them against the campaign's audience
|
|
262
|
+
first. The thank-you is an answer like any other: the user approves it in Operator
|
|
263
|
+
before it is sent.
|
|
264
|
+
|
|
265
|
+
Every first email you draft ends with one plain sentence that lets the prospect opt
|
|
266
|
+
out, such as "If this is not for you, reply and I will not write again." When they do,
|
|
267
|
+
report it as a stop.
|
|
268
|
+
|
|
269
|
+
## After the user approves
|
|
270
|
+
|
|
271
|
+
A `work.execute` task with `action: send_reply` follows. Claim it, send the answer in
|
|
272
|
+
the thread the prospect wrote in, report the result, then close the reply with status
|
|
273
|
+
`handled`, outcome `response_recorded` and the sent message's evidence as
|
|
274
|
+
`responseEvidenceId`. If your answer offers times for a meeting, label that sent message
|
|
275
|
+
`meeting_requested` with the same evidence ("Label a message you sent").
|
|
276
|
+
|
|
277
|
+
## When a meeting is booked
|
|
278
|
+
|
|
279
|
+
- The prospect picks a slot: send the invite from the user's calendar
|
|
280
|
+
(`calendar.event.create`), then record `"meeting": { "at": "…Z", "source": "sent_invite" }`
|
|
281
|
+
in a `replies set` result.
|
|
282
|
+
- The prospect sent an invite: set `"invite": true` with the classification. A
|
|
283
|
+
`meetings.confirm` task follows, assigned to one agent of the person's assignee that
|
|
284
|
+
is allowed to make workspace changes and reports `calendar.event.create` or
|
|
285
|
+
`meeting.book`. It accepts the invite on the
|
|
286
|
+
assignee's calendar and records `"source": "received_invite"`. If the assignee has no
|
|
287
|
+
such agent, no task appears.
|
|
288
|
+
- The meeting moves the person to the playbook's goal stage. Record it once.
|
|
289
|
+
|
|
290
|
+
## Before a meeting is booked
|
|
291
|
+
|
|
292
|
+
Record where an unbooked meeting stands. When the prospect writes about it, use `"meetingState"` in a
|
|
293
|
+
`replies set` result. When you ask for the meeting in a message you sent, label that message
|
|
294
|
+
`meeting_requested` (see "Label a message you sent"):
|
|
295
|
+
|
|
296
|
+
- `requested`: a time was asked for or offered, by them or by you.
|
|
297
|
+
- `accepted`: they agreed, and no time is fixed yet.
|
|
298
|
+
- `declined`: they said no to the meeting. A plain no is not a request to stop; classify the
|
|
299
|
+
reply on its own.
|
|
300
|
+
|
|
301
|
+
Each state is written once, and only when it differs from the last one. Nothing is written after
|
|
302
|
+
a booking. If a playbook step waits for a meeting and the wait runs out while the request is
|
|
303
|
+
`requested` or `accepted`, Audienti first asks you for a follow-up, up to 3 times (the playbook
|
|
304
|
+
can change the count and the wait; the default wait is 5 days). Write each follow-up yourself
|
|
305
|
+
after you read the whole conversation, and send it on the thread's channel. Do not use a fixed
|
|
306
|
+
template unless the user put one in your instructions. Save it as a plan action with the
|
|
307
|
+
`playbookStepKey` the task names. After the last wait runs out, Audienti records
|
|
308
|
+
`meeting_no_answer` itself and tells the owner. Do not record it.
|
|
309
|
+
|
|
310
|
+
## Taken over
|
|
311
|
+
|
|
312
|
+
When the user takes a person over, every open task and draft for them is withdrawn
|
|
313
|
+
and no new task about them appears, in any campaign. There is no hand back. Do not
|
|
314
|
+
contact them again. A draft for them is refused with `taken_over`.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Mailbox checks
|
|
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 and read it for replies. Two queue tasks ask for that. If you have no email
|
|
6
|
+
connector, tell your user exactly: "I need a Gmail or Outlook connector to send from your
|
|
7
|
+
mailbox." Never send them to Settings to connect a mailbox.
|
|
8
|
+
|
|
9
|
+
## The test email (`mailbox.confirm`)
|
|
10
|
+
|
|
11
|
+
A mailbox counts for campaigns only once you sent a test email from it to itself, read it
|
|
12
|
+
back and reported its code. [The mailbox test email](mailbox-check.md) gives the steps. Your
|
|
13
|
+
user does nothing in that check. It proves the mailbox sends and that you can read its
|
|
14
|
+
inbox, which the reply checks below need too.
|
|
15
|
+
|
|
16
|
+
## The reply check (`replies.check`)
|
|
17
|
+
|
|
18
|
+
Before any next step goes to someone you emailed, in a campaign or not, Audienti asks you to
|
|
19
|
+
check the mailbox the email went out from for their reply. Until you report each person, no
|
|
20
|
+
next step is planned or sent for them. Each task lists at most 100 people in `waiting`, each
|
|
21
|
+
with `person`, `name`, `participation` (null when the step is in no campaign), `attempt`
|
|
22
|
+
(the email) and `sentAt` (when it went out). A task for a mailbox you confirm is assigned to
|
|
23
|
+
you.
|
|
24
|
+
While you hold the task, its list stays as it was when you claimed it, even after a reply
|
|
25
|
+
you record. Report everyone on it.
|
|
26
|
+
|
|
27
|
+
1. Claim the task.
|
|
28
|
+
2. Search the mailbox with your connector for mail from the listed people since their
|
|
29
|
+
`sentAt` only. Do not read anything else.
|
|
30
|
+
3. Record each reply you find the way you record any reply: `audienti prospects
|
|
31
|
+
conversation append PERSON --workspace ID --file ITEM.json` with `direction: inbound`
|
|
32
|
+
and `conversationType: direct`. It returns the evidence id. Audienti then ends the
|
|
33
|
+
person's wait, holds their sends and gives you a reply task, as for any reply.
|
|
34
|
+
4. Report every listed person:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"results": [
|
|
39
|
+
{ "kind": "email.no_reply", "person": "PERSON_ID" },
|
|
40
|
+
{ "kind": "email.reply", "person": "PERSON_ID", "evidence": "EVIDENCE_ID" }
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
audienti outcomes report TASK_ID --file RESULTS.json
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
5. Report the task done. If you cannot read the mailbox now, release the task with a note.
|
|
50
|
+
|
|
51
|
+
An `email.reply` whose evidence is not an inbound message of that person is not recorded
|
|
52
|
+
(`reply_not_recorded`): record the reply first. A person you leave out comes back on the
|
|
53
|
+
next check, about an hour later. Reporting the same person again for the same email returns
|
|
54
|
+
`already_recorded`.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: playbook-design
|
|
3
|
+
description: Create, reuse or revise an Audienti playbook, the stages and steps a prospect moves through from first touch to the goal. Use during getting started, when a campaign has no playbook, and whenever you or the user have a new outreach idea.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Playbook design
|
|
7
|
+
|
|
8
|
+
A playbook is what a campaign follows: the stages a prospect moves
|
|
9
|
+
through, the steps in each stage (channel, action, wait), the goal stage, the ways out
|
|
10
|
+
(off-ramps) and the voice. Audienti stores every playbook with revisions, so a good one
|
|
11
|
+
is written once and reused by many campaigns. Planning tasks (`prospects.plan`) read the
|
|
12
|
+
campaign's playbook to decide each prospect's next step, so a campaign without one
|
|
13
|
+
cannot be worked.
|
|
14
|
+
|
|
15
|
+
Use this skill when:
|
|
16
|
+
|
|
17
|
+
- You are getting started and the workspace has no playbook yet.
|
|
18
|
+
- A queue task says a campaign needs a playbook (**Draft a playbook**, or a
|
|
19
|
+
**Get campaign ready** task that says "pick a playbook").
|
|
20
|
+
- You or the user have a new idea for how to reach people: a new channel order, a
|
|
21
|
+
different first touch, a new voice, a new goal.
|
|
22
|
+
|
|
23
|
+
## Rules
|
|
24
|
+
|
|
25
|
+
- Reuse before you create. Most new campaigns fit a playbook that already exists.
|
|
26
|
+
- Use only channels you can cover with accounts you reported (LinkedIn, Email). For anything you cannot do yourself, such as a call, a voicemail, a handwritten note, a video or a gift, use a `Custom` step: you prepare it and a person does it. A Custom step can wait for a named event the person records (`wait.until` `event`), for example "Gift delivered".
|
|
27
|
+
Do not draft a phone step if no one can call.
|
|
28
|
+
- One playbook is one strategy. A correction to the same strategy (a wait, a step's
|
|
29
|
+
wording, a missing off-ramp) is a new revision. A different strategy is a new
|
|
30
|
+
playbook, so the two can be compared in Analytics.
|
|
31
|
+
- Saving a playbook never changes campaigns already pinned to it. Pin the new
|
|
32
|
+
revision on purpose.
|
|
33
|
+
- Give the playbook an off-ramp with reason `rejected` and one with reason `not_fit`. The default
|
|
34
|
+
rules use them: a reply that says no thanks ends the person in the first, and a `not_a_fit` event
|
|
35
|
+
ends the person in the second. Without them the event only shows in the history. The guide
|
|
36
|
+
`docs/guides/playbook-agent-guide.md` (Events and rules) lists every event.
|
|
37
|
+
- Never invent results or proof points for the voice. Use the user's words.
|
|
38
|
+
- Every first email ends with one plain sentence that lets the prospect opt out, such
|
|
39
|
+
as "If this is not for you, reply and I will not write again." Say so in the first
|
|
40
|
+
email step's `instructions`. A prospect who opts out is suppressed for good.
|
|
41
|
+
- Ask the user before a new playbook is used for outreach. Nothing is sent without
|
|
42
|
+
their approval.
|
|
43
|
+
|
|
44
|
+
## 1. Reuse first
|
|
45
|
+
|
|
46
|
+
1. `audienti playbook list` shows one page of summaries: name, latest revision and stage and step
|
|
47
|
+
counts. Follow `nextCursor` for more; `--limit N` sets the page size.
|
|
48
|
+
Every workspace has three starter playbooks (`system: true`): Email first, LinkedIn
|
|
49
|
+
first and Email only. When your reported channels point to one, the list marks it
|
|
50
|
+
`recommended: true` and returns a `recommendation` with its `reason`. A confirmed
|
|
51
|
+
LinkedIn account gives LinkedIn first: the user registered it as a sender account and
|
|
52
|
+
authorized you on it, and your report for it is healthy and includes `send_message`.
|
|
53
|
+
Email with LinkedIn not yet confirmed gives Email first; email alone gives Email only.
|
|
54
|
+
2. `audienti playbook show ID` shows the stages, steps, goal and voice.
|
|
55
|
+
3. If one fits the campaign's offer, audience and your channels, use it. Say in one
|
|
56
|
+
line why it fits, then go to step 4 (pin it). A starter playbook can be pinned as it
|
|
57
|
+
is, but it is read-only: to change it, save its fields as a new playbook (no `id`,
|
|
58
|
+
`expectedRevision` 0) with a new name, and revise that copy. Saving a starter
|
|
59
|
+
playbook with its `id` fails with 409 `starter_playbook`.
|
|
60
|
+
|
|
61
|
+
## 2. Draft a new playbook
|
|
62
|
+
|
|
63
|
+
Start from the user's offer, the audience and the channels you have. A good default
|
|
64
|
+
for founder-led or agent-led outbound has five to six stages:
|
|
65
|
+
|
|
66
|
+
1. **Ready** (Agent): the prospect is in the campaign. No steps.
|
|
67
|
+
2. **Warming** (Agent, optional): view the profile, like a recent post. Skip it when
|
|
68
|
+
LinkedIn is not available.
|
|
69
|
+
3. **Outreach underway** (Agent): the touches, in order, with waits. For example a
|
|
70
|
+
connection request (wait 21 days), a message "Only if connected" (wait 5), a first
|
|
71
|
+
email (wait 4). Completes to the **Unresponsive** off-ramp when every step is done
|
|
72
|
+
and nobody replied.
|
|
73
|
+
4. **In conversation** (Assignee): the prospect replied. A person drafts or approves
|
|
74
|
+
the reply. Completion `stay`.
|
|
75
|
+
5. **Meeting requested** (Assignee).
|
|
76
|
+
6. **Meeting booked** (Assignee, goal, `goalDays` 0).
|
|
77
|
+
|
|
78
|
+
Write `instructions` on each step as coaching for whoever does it: what to say, what
|
|
79
|
+
evidence to lead with, what to avoid. Write `voice` as two or three sentences in the
|
|
80
|
+
user's style. See [fields and an example request](references/fields.md) for every
|
|
81
|
+
field and the rules Audienti checks.
|
|
82
|
+
|
|
83
|
+
## 3. Save it
|
|
84
|
+
|
|
85
|
+
1. Write the request to a file: `expectedRevision` 0 and no `id` for a new playbook;
|
|
86
|
+
the playbook's `id` and its current revision number to revise one. Use a fresh
|
|
87
|
+
`requestId` (a UUID) and fresh UUIDs for every stage, step and off-ramp `key`.
|
|
88
|
+
Keep the same keys when you revise, so progress already recorded stays on its stage.
|
|
89
|
+
2. `audienti playbook save --file REQUEST.json`. Over MCP use `playbook_save`.
|
|
90
|
+
3. If the save fails, read the error: `field` is the path of the wrong value, `allowed` the
|
|
91
|
+
values that work and `next` what to do. Fix that field and retry with the same
|
|
92
|
+
`requestId`. Run `audienti playbook actions` for the step action ids, channels and event
|
|
93
|
+
names. If `next` says to ask your user (for example to turn on Allow workspace changes),
|
|
94
|
+
ask with `audienti ask` and carry on with what you can. If it reports a revision conflict, the error gives `currentRevision`: run
|
|
95
|
+
`playbook show ID`, apply your change to that revision and save with that `expectedRevision`.
|
|
96
|
+
|
|
97
|
+
## 4. Ask the user and pin it
|
|
98
|
+
|
|
99
|
+
1. Ask in Audienti, not in chat: `audienti ask "Use this playbook for CAMPAIGN?"
|
|
100
|
+
--context "SUMMARY" --option "Use it" --option "Change it"`. Put the stages, the
|
|
101
|
+
touches with their waits, the goal and the voice in the context as a short list.
|
|
102
|
+
2. When they answer, apply their changes as a new revision.
|
|
103
|
+
3. Pin it to the campaign with `playbookPin` `{ "id": PLAYBOOK_ID, "revision": N }`:
|
|
104
|
+
in `audienti campaign create` for a new campaign, or in the campaign's draft update
|
|
105
|
+
for a draft. An active campaign keeps its pin; to try a new playbook, make a
|
|
106
|
+
successor (below).
|
|
107
|
+
|
|
108
|
+
## New ideas
|
|
109
|
+
|
|
110
|
+
When you or the user have a new idea for how to reach people:
|
|
111
|
+
|
|
112
|
+
1. Write the idea as one change: "Lead with email instead of a connection request",
|
|
113
|
+
"Add a phone step after the second email", "Offer a teardown, not a demo".
|
|
114
|
+
2. If it is a correction, save a new revision of the same playbook.
|
|
115
|
+
3. If it is a different strategy, save a new playbook named for the idea.
|
|
116
|
+
4. Test it with `audienti campaign successor ID` that changes only the playbook, so
|
|
117
|
+
the old and new campaigns can be compared. Name what changed in the campaign notes.
|
|
118
|
+
5. Ask the user before the successor starts.
|
|
119
|
+
|
|
120
|
+
## Record progress and ask when the rules cannot decide
|
|
121
|
+
|
|
122
|
+
Read the playbook before doing work for a campaign pinned to it: `audienti playbook
|
|
123
|
+
show PLAYBOOK_ID`. Stage plays are instructions for you, the external agent; they are
|
|
124
|
+
not a hosted sequence that runs by itself.
|
|
125
|
+
|
|
126
|
+
- Progress: `audienti playbook record --file PATH` with exactly one of `stageKey`,
|
|
127
|
+
`offRampKey` or `finishStage: true`, the participation's pinned playbook version and
|
|
128
|
+
its current expected progress version, and stored evidence. Do not invent outcomes.
|
|
129
|
+
`audienti playbook` prints the command usage; the API schema has the required fields.
|
|
130
|
+
- When you see something that moves a person (a reply, an accept, a booked meeting), record
|
|
131
|
+
that event; the playbook's rules decide the move. If the rules cannot decide, ask the
|
|
132
|
+
user with `audienti ask "QUESTION"`. The question appears in Operator.
|
|
133
|
+
- None of these commands sends a message to a provider.
|
|
134
|
+
|
|
135
|
+
## Done
|
|
136
|
+
|
|
137
|
+
Close your queue task with a note that names the playbook, its revision and the
|
|
138
|
+
campaign it is pinned to: `audienti queue done TASK --note "Saved Founder-led outbound
|
|
139
|
+
r1, pinned to Logistics UK test"`.
|