@audienti/cli 0.2.0-exora.7 → 0.2.0-exora.9

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.
@@ -4,6 +4,6 @@ import {z} from 'zod';
4
4
  const uuid=z.string().uuid(),maxBytes=5*1024*1024;
5
5
  const base64=z.string().min(4).max(4*Math.ceil(maxBytes/3)+8).refine(value=>value.length%4===0&&/^[A-Za-z0-9+/]+={0,2}$/.test(value));
6
6
  export const attachmentOperations={
7
- attachment_upload:{schema:z.object({workspaceId:uuid.optional(),name:z.string().trim().min(1).max(255),contentType:z.string().max(127),contentBase64:base64,durationSeconds:z.number().int().positive().max(120).optional()}).strict(),method:'POST',path:a=>`${a.workspaceId?`/workspaces/${a.workspaceId}`:''}/attachments`,body:a=>({name:a.name,contentType:a.contentType,contentBase64:a.contentBase64,...(a.durationSeconds?{durationSeconds:a.durationSeconds}:{})}),agentWide:true,description:'Store an image (jpeg, png, gif, webp; at most 5 MB) or a voice note (mpeg, mp4, ogg, wav, webm; at most 5 MB and 2 minutes) and return its id. Name the id in content.attachments of a linkedin.send action tagged profile.message.send. A voice note needs durationSeconds and goes only to a connection. Needs workspace:access.'},
8
- attachment_get:{schema:z.object({workspaceId:uuid.optional(),attachmentId:uuid}).strict(),method:'GET',path:a=>`${a.workspaceId?`/workspaces/${a.workspaceId}`:''}/attachments/${a.attachmentId}`,agentWide:true,description:'One uploaded file with its content as base64. An agent that sends from its own LinkedIn account fetches the file here, then attaches it in LinkedIn.'},
7
+ attachment_upload:{schema:z.object({workspaceId:uuid.optional(),name:z.string().trim().min(1).max(255),contentType:z.string().max(127),contentBase64:base64,durationSeconds:z.number().int().positive().max(120).optional()}).strict(),method:'POST',path:a=>`${a.workspaceId?`/workspaces/${a.workspaceId}`:''}/attachments`,body:a=>({name:a.name,contentType:a.contentType,contentBase64:a.contentBase64,...(a.durationSeconds?{durationSeconds:a.durationSeconds}:{})}),scoped:true,description:'Store an image (jpeg, png, gif, webp; at most 5 MB) or a voice note (mpeg, mp4, ogg, wav, webm; at most 5 MB and 2 minutes) and return its id. Name the id in content.attachments of a linkedin.send action tagged profile.message.send. A voice note needs durationSeconds and goes only to a connection. Needs workspace:access.'},
8
+ attachment_get:{schema:z.object({workspaceId:uuid.optional(),attachmentId:uuid}).strict(),method:'GET',path:a=>`${a.workspaceId?`/workspaces/${a.workspaceId}`:''}/attachments/${a.attachmentId}`,scoped:true,description:'One uploaded file with its content as base64. An agent that sends from its own LinkedIn account fetches the file here, then attaches it in LinkedIn.'},
9
9
  };
package/cli/audienti.mjs CHANGED
@@ -7,7 +7,8 @@ const usage = `Profiles: audienti profiles list|search|get ID | match evaluate|a
7
7
  Skills: audienti skills list [--compact]|show SKILL|read SKILL RESOURCE|helper NAME --file INPUT.json|- [--json]
8
8
  Tools: audienti call TOOL ['JSON'] | tools search "WORDS"|get TOOL|list|runs|balance [--help]
9
9
  Sender: audienti sender accounts list|get|alias|register|bind|unbind|state|automation|activity report [--profile PATH]
10
- Managed LinkedIn: audienti sender managed list|connect [--profile PATH] (connect: the user only; returns a sign-in link)
10
+ Managed LinkedIn: audienti sender managed list|connect [--workspace ID] [--profile PATH] (connect: the user only; returns a sign-in link)
11
+ audienti sender managed plan MANAGED_ACCOUNT_ID --plan free|premium_career|premium_business|premium_all_in_one|sales_navigator_core|sales_navigator_advanced|recruiter_lite|none [--workspace ID] (the user only; sets the connection note length; none clears it)
11
12
  Prospecting: audienti prospecting settings show|save|accounts|account-show|account-save|history [--profile PATH]
12
13
  Usage: audienti whoami [--json]
13
14
  audienti workspace assignments list [--workspace ID]
@@ -75,14 +76,16 @@ Discovery: prospects list|get ID|timeline ID [--limit N]|observation create|cita
75
76
  budgets show --workspace ID
76
77
  budgets alerts --workspace ID [--limit N]
77
78
  usage breakdown [--workspace ID] [--since month|7d|30d|90d] [--by tool|provider|model|caller|day] [--format json|csv]
79
+ workspace usage summary|callers|calls [--after ID] [--caller user:ID|agent:ID|key:ID] [--status all|succeeded|failed|pending]|call ID [--workspace ID] (paid tool use, last 30 days, and single calls)
78
80
  audit list [--workspace ID] [--since 7d] [--actor agent|person] [--category NAME] [--format json|csv]
79
81
  budgets set --workspace ID [--agent ID --cap N|none] [--new-agent-cap N|none] [--at-limit pause|ask] [--alert80 on|off] (alert80 covers Operator and email at 80%)
80
82
  Qualification: campaigns qualification submit --file INPUT.json
83
+ Campaign summary: campaign summary [--workspace ID] (campaign counts per state, and per campaign its audience, offer, playbook, goal stage, people, engaged and at-goal counts)
81
84
  Find prospects: campaign find CAMPAIGN_ID --count N --max-credits C (searches with the audience filters, checks each person, admits only fits; stops at N fits or the credit cap and reports the spend)
82
85
  campaign check CAMPAIGN_ID --url LINKEDIN_URL [--request-id UUID] (checks one person the same way; reuse the request ID after an interruption)
83
86
  Target companies: campaign targets CAMPAIGN_ID [--limit N --after CURSOR] (reads the companies people must work at to join)
84
87
  campaign targets CAMPAIGN_ID --add acme.com,linkedin.com/company/globex [--remove old.com] | --set FILE|- (one company per line; saves a new revision)
85
- Playbooks: playbook list [--limit N --cursor ID]|show ID [--revision N]|save --file REQUEST.json|progress PARTICIPATION_ID|record --file REQUEST.json|suggest --file REQUEST.json|suggestions|decide --file REQUEST.json (audienti playbook SUBCOMMAND --help for the request shape)
88
+ Playbooks: playbook list [--archived] [--limit N --cursor ID]|show ID [--revision N]|save --file REQUEST.json|archive ID|restore ID|delete ID|progress PARTICIPATION_ID|record --file REQUEST.json|suggest --file REQUEST.json|suggestions|decide --file REQUEST.json (audienti playbook SUBCOMMAND --help for the request shape)
86
89
  Analytics: analytics show --from ISO --to ISO [--campaign ID] [--playbook-version ID] [--format json|csv]
87
90
  analytics outbound [--workspace ID] [--days N | --from ISO --to ISO] [--by sender|action|day] [--format json|csv]
88
91
  analytics activity [--workspace ID] [--days N | --from ISO --to ISO] [--format json|csv]
package/cli/campaigns.mjs CHANGED
@@ -34,8 +34,12 @@ export async function campaignCli(args){
34
34
  input.expectedRevision=flags['expected-revision']!==undefined?Number(flags['expected-revision']):(await callWorkspace('campaign_targets_get',{workspaceId:flags.workspace,campaignId:position[2],limit:1},config)).data.revision;
35
35
  console.log(JSON.stringify(await callWorkspace('campaign_targets_change',input,config)));return;
36
36
  }
37
+ if(command==='summary'){
38
+ if(position.length!==2)throw new Error('Use campaign summary [--workspace ID].');
39
+ console.log(JSON.stringify(await callWorkspace('campaign_summary',{workspaceId:flags.workspace},configuration(flags.profile))));return;
40
+ }
37
41
  const name={list:'campaign_list',show:'campaign_get',create:'campaign_create',update:'campaign_update',transition:'campaign_transition',successor:'campaign_successor',events:'campaign_events',pace:flags['per-day']===undefined&&flags['max-credits']===undefined?'campaign_pace_get':'campaign_pace_set'}[command];
38
- if(!name||position.length!==(['list','create'].includes(command)?2:3))throw new Error('Use campaign list|show ID|create|update ID|transition ID|successor ID|events ID|pace ID [--per-day N|derived] [--max-credits C|none]|find ID --count N --max-credits C|check ID --url URL|targets ID [--add|--remove|--set].');
42
+ if(!name||position.length!==(['list','create'].includes(command)?2:3))throw new Error('Use campaign list|summary|show ID|create|update ID|transition ID|successor ID|events ID|pace ID [--per-day N|derived] [--max-credits C|none]|find ID --count N --max-credits C|check ID --url URL|targets ID [--add|--remove|--set].');
39
43
  if(flags['per-day']!==undefined&&command!=='pace')throw new Error('Set the pace with campaign pace ID --per-day N|derived, after the campaign is saved.');
40
44
  const input={workspaceId:flags.workspace};
41
45
  if(position[2])input.campaignId=position[2];
@@ -3,16 +3,16 @@
3
3
  // playbook-design skill: audienti skills read playbook-design references/fields.md.
4
4
  export const playbookCommands = {
5
5
  list: {
6
- usage: 'playbook list [--limit 1..100] [--cursor ID] [--workspace ID] [--profile PATH]',
7
- about: 'One page of summaries, 50 by default: id, name, currentRevision, system, systemKey, stages, steps, recommended, updatedAt. Starters have system true and cannot be changed; clone one by saving its fields without an id. Use nextCursor as --cursor for the next page. Full fields come from playbook show.'
6
+ usage: 'playbook list [--archived] [--limit 1..100] [--cursor ID] [--workspace ID] [--profile PATH]',
7
+ about: 'One page of summaries, 50 by default: id, name, currentRevision, system, systemKey, archivedAt, stages, steps, recommended, updatedAt. Archived playbooks are hidden; --archived lists them instead. Starters have system true and cannot be changed; clone one by saving its fields without an id. Use nextCursor as --cursor for the next page. Full fields come from playbook show.'
8
8
  },
9
9
  show: {
10
10
  usage: 'playbook show PLAYBOOK_ID [--revision N] [--workspace ID] [--profile PATH]',
11
- about: 'The whole playbook (fields) and the campaigns that use it. Without --revision it returns the current one. PLAYBOOK_ID comes from playbook list.'
11
+ about: 'The whole playbook (fields) and the campaigns that use it. Without --revision it returns the current one. PLAYBOOK_ID comes from playbook list. warnings is the Playbook check: what is wrong with the playbook (level error or warning, a message, the stage and step, and the default fix). An empty list means nothing to look at.'
12
12
  },
13
13
  save: {
14
14
  usage: 'playbook save --file REQUEST.json [--workspace ID] [--profile PATH]',
15
- about: 'Create a playbook or save a new revision. The file is {"requestId": UUID, "expectedRevision": N, "id": PLAYBOOK_ID, "fields": {...}}. requestId: a fresh UUID; reuse it only to retry the same save. id: leave it out for a new playbook. expectedRevision: 0 for a new playbook, otherwise currentRevision from playbook show. A save from a stale revision is refused with revision_conflict and the response gives currentRevision. fields: name, stages (2 or more, each with a UUID key), goalKey (a stage key after the first stage), offRamps. Field rules: audienti skills read playbook-design references/fields.md',
15
+ about: 'Create a playbook or save a new revision. The file is {"requestId": UUID, "expectedRevision": N, "id": PLAYBOOK_ID, "fields": {...}}. requestId: a fresh UUID; reuse it only to retry the same save. id: leave it out for a new playbook. expectedRevision: 0 for a new playbook, otherwise currentRevision from playbook show. A save from a stale revision is refused with revision_conflict and the response gives currentRevision. A playbook that cannot work (no off-ramp, or a stage nobody can reach) is refused with playbook_cannot_work and the list in errors. A saved playbook comes back with warnings, the Playbook check, for things that are legal but probably wrong. fields: name, stages (2 or more, each with a UUID key), goalKey (a stage key after the first stage), offRamps. Field rules: audienti skills read playbook-design references/fields.md',
16
16
  example: {
17
17
  requestId: 'UUID',
18
18
  expectedRevision: 0,
@@ -27,6 +27,18 @@ export const playbookCommands = {
27
27
  }
28
28
  }
29
29
  },
30
+ archive: {
31
+ usage: 'playbook archive PLAYBOOK_ID [--workspace ID] [--profile PATH]',
32
+ about: 'Hide a playbook from the list and the campaign picker. It cannot be edited or newly chosen for a campaign until you restore it. Campaigns that already use it keep running on their version. Safe to repeat. Starters can be archived.'
33
+ },
34
+ restore: {
35
+ usage: 'playbook restore PLAYBOOK_ID [--workspace ID] [--profile PATH]',
36
+ about: 'Bring an archived playbook back. Find archived playbooks with playbook list --archived. Safe to repeat.'
37
+ },
38
+ delete: {
39
+ usage: 'playbook delete PLAYBOOK_ID [--workspace ID] [--profile PATH]',
40
+ about: 'Delete a playbook and all its versions. This cannot be undone. A playbook that any campaign uses, in any state, is refused with playbook_in_use and the campaigns are named; archive it instead. Starters cannot be deleted.'
41
+ },
30
42
  progress: {
31
43
  usage: 'playbook progress PARTICIPATION_ID [--workspace ID] [--profile PATH]',
32
44
  about: 'The stage and outcome history of one person in one campaign. It takes a participation ID, not a playbook ID. A participation exists once a person is admitted to a campaign. Each person in the output of pipeline show has a participationId.'
@@ -9,17 +9,22 @@ const eventRule=z.object({key:z.string().uuid(),when:z.string().min(1).max(60).d
9
9
  const stepRoutes=z.object({accept:routeTarget.optional(),reply:routeTarget.optional(),meeting:routeTarget.optional(),timeout:routeTarget.optional()}).strict().describe("Where an event that reaches the person after this step ran sends them. A route wins over the stage's jump list; an event with no route follows the stage. Going to an earlier stage needs rules.allowBackward.");
10
10
  const playbookStepSchema=z.object({key:z.string().uuid(),channel:z.enum(['LinkedIn','Email']),action:label,condition:z.enum(['Always','Only if viable','Only if connected']),viability:text.optional(),instructions:text.optional(),wait:z.object({until:z.enum(['accept','reply','meeting','timeout']),days:z.number().int().min(1).max(60),followUps:z.number().int().min(0).max(10).optional().describe('Meeting waits only: how many follow-ups your agent writes, one after each wait, before the meeting is marked No answer. Default 3.')}).strict().optional(),waitDays:z.number().int().min(1).max(60).optional().describe('Older form of wait: the same as wait {until: timeout, days}.'),side:z.boolean().optional(),onTimeout:z.enum(['next','off_ramp']).optional(),humanize:z.boolean().optional().describe('Email only. Rewrite the email text before approval so it reads as written by a person. On by default; set false to send the text as written.'),actionId:z.string().max(80).optional().describe('An action id from the outreach rulebook, for example linkedin.message.send.'),routes:stepRoutes.optional(),eventRules:z.array(eventRule).max(10).optional()}).strict();
11
11
  const completion=z.discriminatedUnion('kind',[z.object({kind:z.literal('next')}).strict(),z.object({kind:z.literal('stay')}).strict(),z.object({kind:z.literal('off_ramp'),offRampKey:z.string().uuid()}).strict()]);
12
- const stage=identity.extend({movesInWhen:text.optional(),owner:z.enum(['Agent','Assignee']).optional(),goalDays:z.number().int().min(0).max(365).optional(),optional:z.boolean().optional(),enabled:z.boolean().optional(),steps:z.array(playbookStepSchema).max(20).optional(),completion:completion.optional(),on:z.array(z.enum(['accept','reply','meeting','manual'])).max(4).optional(),eventRules:z.array(eventRule).max(10).optional()});
13
- // A playbook saved before conflict 20 may carry autoMove; it is dropped, because approval lives on the account.
14
- const dropAutoMove=value=>{if(!value||typeof value!=='object'||Array.isArray(value)||!('autoMove' in value))return value;const {autoMove:_dropped,...rest}=value;return rest;};
15
- const playbookRulesSchema=z.preprocess(dropAutoMove,z.object({flagOverdue:z.boolean().optional(),allowBackward:z.boolean().optional(),emailWindow:z.enum(['weekdays_morning','weekdays','any_day']).optional(),nurtureDays:z.union([z.literal(60),z.literal(90),z.literal(180),z.null()]).optional()}).strict());
12
+ const stageFields=identity.extend({movesInWhen:text.optional(),owner:z.enum(['Agent','Assignee']).optional(),goalDays:z.number().int().min(0).max(365).optional(),enabled:z.boolean().optional().describe('false skips the stage: people are never moved into it. The first stage and the goal stage cannot be skipped.'),steps:z.array(playbookStepSchema).max(20).optional(),completion:completion.optional(),on:z.array(z.enum(['accept','reply','meeting','manual'])).max(4).optional(),eventRules:z.array(eventRule).max(10).optional()});
13
+ // Settings the product no longer asks for (#457). An older request may still carry them: they are accepted, ignored and left out of the new version.
14
+ // autoMove: conflict 20, approval lives on the account. flagOverdue, emailWindow, nurtureDays: no code read them. optional (stage): use enabled.
15
+ const dropKeys=keys=>value=>{if(!value||typeof value!=='object'||Array.isArray(value)||!keys.some(key=>key in value))return value;return Object.fromEntries(Object.entries(value).filter(([key])=>!keys.includes(key)));};
16
+ const stage=z.preprocess(dropKeys(['optional']),stageFields);
17
+ const playbookRulesSchema=z.preprocess(dropKeys(['autoMove','flagOverdue','emailWindow','nurtureDays']),z.object({allowBackward:z.boolean().optional().describe('Let a move go to an earlier stage, for example when a meeting is cancelled. Default false.')}).strict());
16
18
  const playbookFieldsSchema=z.object({name:label,stages:z.array(stage).min(2).max(30),goalKey:z.string().uuid(),offRamps:z.array(identity.extend({reason:z.enum(['rejected','not_fit','nurture','unresponsive','other','taken_over','bounced','skipped'])})).max(30),voice:text.optional(),rules:playbookRulesSchema.optional(),eventRules:z.array(eventRule).max(30).optional()}).strict();
17
19
  const fields=playbookFieldsSchema;
18
20
  const base=a=>`/workspaces/${a.workspaceId}`,query=(a,skip=[])=>new URLSearchParams(Object.entries(a).filter(([k,v])=>k!=='workspaceId'&&!skip.includes(k)&&v!==undefined).map(([k,v])=>[k,String(v)]));
19
21
  export const playbookOperations={
20
- playbook_list:{schema:z.object({...scope,cursor:uuid.optional().describe('The nextCursor of the previous page.'),limit:z.number().int().min(1).max(100).optional().describe('Playbooks per page. Default 50.')}).strict(),method:'GET',path:a=>`${base(a)}/playbooks?${query(a)}`,scoped:true,description:'List the playbooks in the current workspace, one page of summaries (id, name, currentRevision, system, systemKey, stages, steps, recommended, updatedAt) with nextCursor. Read the fields of one with playbook_get. Starter playbooks (Email first, LinkedIn first, Email only) carry system:true and are read-only; clone one by saving its fields without an id. recommended marks the starter playbook that fits the channels your agents reported, and recommendation gives its reason in one line. Free CRM read.'},
21
- playbook_get:{schema:z.object({...scope,id:uuid,revision:z.number().int().positive().optional()}).strict(),method:'GET',path:a=>`${base(a)}/playbooks/${a.id}?${query(a,['id'])}`,scoped:true,description:'Read a playbook version: stage plays, step waits, completion, goal, voice and agent rules. Instructions are executed by the external agent, never by a hosted sequence runner.'},
22
- playbook_save:{schema:z.object({...scope,id:uuid.optional(),requestId:uuid,expectedRevision:z.number().int().nonnegative().describe('0 for a new playbook, otherwise currentRevision from playbook_get. A stale value is refused with revision_conflict, which carries currentRevision.'),fields}).strict(),method:'POST',path:a=>`${base(a)}/playbooks`,body:({workspaceId,...a})=>a,scoped:true,description:'Create or revise a playbook with optimistic version and idempotent request. A starter playbook refuses revision (409 starter_playbook); save its fields without an id to make an editable copy. Does not change existing campaign or participation pins.'},
22
+ playbook_list:{schema:z.object({...scope,cursor:uuid.optional().describe('The nextCursor of the previous page.'),limit:z.number().int().min(1).max(100).optional().describe('Playbooks per page. Default 50.'),archived:z.boolean().optional().describe('true lists the archived playbooks instead of the active ones. Default false.')}).strict(),method:'GET',path:a=>`${base(a)}/playbooks?${query(a)}`,scoped:true,description:'List the playbooks in the current workspace, one page of summaries (id, name, currentRevision, system, systemKey, archivedAt, stages, steps, check {errors, warnings}, recommended, updatedAt) with nextCursor. Archived playbooks are left out unless archived is true. Read the fields of one with playbook_get. Starter playbooks (Email first, LinkedIn first, Email only) carry system:true and are read-only; clone one by saving its fields without an id. recommended marks the starter playbook that fits the channels your agents reported, and recommendation gives its reason in one line. Free CRM read.'},
23
+ playbook_get:{schema:z.object({...scope,id:uuid,revision:z.number().int().positive().optional()}).strict(),method:'GET',path:a=>`${base(a)}/playbooks/${a.id}?${query(a,['id'])}`,scoped:true,description:'Read a playbook version: stage plays, step waits, completion, goal, voice and agent rules. Instructions are executed by the external agent, never by a hosted sequence runner. warnings is the Playbook check (level error or warning, message, stageKey, stepKey, fix): the same list the editor and the campaign picker show.'},
24
+ playbook_archive:{schema:z.object({...scope,id:uuid}).strict(),method:'POST',path:a=>`${base(a)}/playbooks/${a.id}/archive`,scoped:true,description:'Archive a playbook: it leaves the list and the campaign picker, and cannot be edited or newly chosen for a campaign until restored. Campaigns that already use it keep running on the version they were set up with. Safe to repeat. Starter playbooks can be archived.'},
25
+ playbook_restore:{schema:z.object({...scope,id:uuid}).strict(),method:'POST',path:a=>`${base(a)}/playbooks/${a.id}/restore`,scoped:true,description:'Bring an archived playbook back to the list and the campaign picker. Safe to repeat.'},
26
+ playbook_delete:{schema:z.object({...scope,id:uuid}).strict(),method:'DELETE',path:a=>`${base(a)}/playbooks/${a.id}`,scoped:true,description:'Delete a playbook and all its versions. This cannot be undone. A playbook that any campaign uses (a campaign of any state, finished or archived included, that was ever set up with a version of it) is refused with 409 playbook_in_use, naming the campaigns; archive it instead. Starter playbooks are refused with starter_playbook.'},
27
+ playbook_save:{schema:z.object({...scope,id:uuid.optional(),requestId:uuid,expectedRevision:z.number().int().nonnegative().describe('0 for a new playbook, otherwise currentRevision from playbook_get. A stale value is refused with revision_conflict, which carries currentRevision.'),fields}).strict(),method:'POST',path:a=>`${base(a)}/playbooks`,body:({workspaceId,...a})=>a,scoped:true,description:'Create or revise a playbook with optimistic version and idempotent request. A starter playbook refuses revision (409 starter_playbook); save its fields without an id to make an editable copy. Does not change existing campaign or participation pins. A playbook that cannot work (no off-ramp, or a stage nobody can reach) is refused with 422 playbook_cannot_work and the list in errors. A saved playbook returns warnings, the Playbook check, for what is legal but probably wrong.'},
23
28
  workspace_pipeline_get:{schema:z.object({...scope,campaignId:uuid.optional(),pinId:uuid.optional()}).strict(),method:'GET',path:a=>`${base(a)}/pipeline?${query(a)}`,scoped:true,description:'Read the pipeline board: stages of one playbook version, the people currently in each stage, and counts of people closed out through each off-ramp. Free CRM read.'},
24
29
  playbook_progress_get:{schema:z.object({...scope,participationId:uuid.describe('A participation ID (one person in one campaign, from the pipeline), not a playbook ID.')}).strict(),method:'GET',path:a=>`${base(a)}/participations/${a.participationId}/progress`,scoped:true,description:'Read the participation’s admitted playbook pin and recorded stage/outcome history. Takes a participation ID (a person in a campaign; each person in the pipeline has one), not a playbook ID.'},
25
30
  playbook_progress_record:{schema:z.object({...scope,participationId:uuid,expectedVersion:z.number().int().nonnegative(),requestId:uuid,stageKey:uuid.optional(),offRampKey:uuid.optional(),finishStage:z.literal(true).optional(),reason:z.string().trim().min(1).max(2000),evidence:z.array(z.object({kind:z.enum(['observation','conversation']),id:uuid}).strict()).max(20)}).strict(),method:'POST',path:a=>`${base(a)}/participations/${a.participationId}/progress`,body:({workspaceId,participationId,...a})=>a,scoped:true,description:'Report a stage, off-ramp, or finishStage:true after externally completing the current play and waits. Finish resolves next/stay/off-ramp from the admitted version. Stored evidence required for agents; human-review and backward-move rules apply. Assigned owner/Admin only. Goal ends prospecting; post-goal forward reporting never reactivates. No send or provider call.'},
package/cli/progress.mjs CHANGED
@@ -2,14 +2,15 @@ import {callWorkspace,configuration} from './workspaces.mjs';
2
2
  import {readJsonFile} from './json-file.mjs';
3
3
  import {playbookUsageError,playbookCommands} from './playbook-help.mjs';
4
4
  export async function progressCli(args){
5
- const positions=[],flags={};for(let i=0;i<args.length;i++){const arg=args[i];if(!arg.startsWith('--')){positions.push(arg);continue;}const key=arg.slice(2);if(!['profile','workspace','file','cursor','limit','revision','from','to','campaign','playbook-version','kind','user','format','owner','offer','audience','tag'].includes(key)||!args[i+1]||args[i+1].startsWith('--')||Object.hasOwn(flags,key))throw new Error(`Unknown or incomplete option --${key}.${args[0]==='playbook'?' Run audienti playbook SUBCOMMAND --help.':''}`);flags[key]=args[++i];}
6
- const names={playbook:{list:'playbook_list',show:'playbook_get',save:'playbook_save',progress:'playbook_progress_get',record:'playbook_progress_record',suggest:'playbook_progress_suggest',suggestions:'playbook_suggestions_list',decide:'playbook_suggestion_decide'},analytics:{show:'workspace_analytics_get'},pipeline:{show:'workspace_pipeline_get'},connections:{list:'workspace_connections_list',register:'workspace_connection_register',report:'workspace_connection_report',disconnect:'workspace_connection_disconnect'}};
5
+ const positions=[],flags={};for(let i=0;i<args.length;i++){const arg=args[i];if(!arg.startsWith('--')){positions.push(arg);continue;}const key=arg.slice(2);if(!['profile','workspace','file','cursor','limit','revision','from','to','campaign','playbook-version','kind','user','format','owner','offer','audience','tag','archived'].includes(key)||(key!=='archived'&&(!args[i+1]||args[i+1].startsWith('--')))||Object.hasOwn(flags,key))throw new Error(`Unknown or incomplete option --${key}.${args[0]==='playbook'?' Run audienti playbook SUBCOMMAND --help.':''}`);flags[key]=key==='archived'&&(!args[i+1]||args[i+1].startsWith('--'))?'true':args[++i];}
6
+ const names={playbook:{list:'playbook_list',show:'playbook_get',save:'playbook_save',archive:'playbook_archive',restore:'playbook_restore',delete:'playbook_delete',progress:'playbook_progress_get',record:'playbook_progress_record',suggest:'playbook_progress_suggest',suggestions:'playbook_suggestions_list',decide:'playbook_suggestion_decide'},analytics:{show:'workspace_analytics_get'},pipeline:{show:'workspace_pipeline_get'},connections:{list:'workspace_connections_list',register:'workspace_connection_register',report:'workspace_connection_report',disconnect:'workspace_connection_disconnect'}};
7
7
  const name=names[positions[0]]?.[positions[1]];if(!name)throw new Error(positions[0]==='playbook'?playbookUsageError:'Use analytics show --from ISO --to ISO; pipeline show; connections list|register --file PATH|report --file PATH|disconnect --file PATH.');
8
8
  if(flags.format&&(name!=='workspace_analytics_get'||!['json','csv'].includes(flags.format)))throw new Error('--format json|csv is only accepted for analytics show.');
9
- const fileNames=['playbook_save','playbook_progress_record','playbook_progress_suggest','workspace_connection_register','workspace_connection_report','playbook_suggestion_decide','workspace_connection_disconnect'];const withId=['playbook_get','playbook_progress_get'].includes(name);if(positions.length!==(withId?3:2))throw new Error(positions[0]==='playbook'?`Unexpected arguments. Usage: audienti ${playbookCommands[positions[1]].usage}`:'Unexpected arguments.');
9
+ const fileNames=['playbook_save','playbook_progress_record','playbook_progress_suggest','workspace_connection_register','workspace_connection_report','playbook_suggestion_decide','workspace_connection_disconnect'];const withId=['playbook_get','playbook_progress_get','playbook_archive','playbook_restore','playbook_delete'].includes(name);if(positions.length!==(withId?3:2))throw new Error(positions[0]==='playbook'?`Unexpected arguments. Usage: audienti ${playbookCommands[positions[1]].usage}`:'Unexpected arguments.');
10
10
  if(fileNames.includes(name)&&!flags.file)throw new Error('Supply a JSON request with --file PATH.');if(!fileNames.includes(name)&&flags.file)throw new Error('--file is only accepted for mutations.');
11
- const input=fileNames.includes(name)?readJsonFile(flags.file,65536,'Request file'):{};if(!input||typeof input!=='object'||Array.isArray(input))throw new Error('Request must be an object.');if(flags.workspace)input.workspaceId=flags.workspace;if(withId)input[name==='playbook_get'?'id':'participationId']=positions[2];
12
- for(const[key,value]of Object.entries(flags)){if(['workspace','profile','file','format'].includes(key))continue;const field={campaign:'campaignId','playbook-version':'pinId',user:'userId',owner:'ownerUserId',offer:'offerId',audience:'audienceId'}[key]??key;input[field]=['limit','revision'].includes(key)?Number(value):value;}
11
+ const input=fileNames.includes(name)?readJsonFile(flags.file,65536,'Request file'):{};if(!input||typeof input!=='object'||Array.isArray(input))throw new Error('Request must be an object.');if(flags.workspace)input.workspaceId=flags.workspace;if(withId)input[name==='playbook_progress_get'?'participationId':'id']=positions[2];
12
+ if(flags.archived!==undefined&&(name!=='playbook_list'||!['true','false'].includes(flags.archived)))throw new Error(name==='playbook_list'?'--archived takes true or false, or no value.':'--archived is only accepted for playbook list.');
13
+ for(const[key,value]of Object.entries(flags)){if(['workspace','profile','file','format'].includes(key))continue;if(key==='archived'){input.archived=value==='true';continue;}const field={campaign:'campaignId','playbook-version':'pinId',user:'userId',owner:'ownerUserId',offer:'offerId',audience:'audienceId'}[key]??key;input[field]=['limit','revision'].includes(key)?Number(value):value;}
13
14
  const result=await callWorkspace(name,input,configuration(flags.profile));
14
15
  if(flags.format==='csv'){process.stdout.write(analyticsCsv(result?.data??result));return;}
15
16
  console.log(JSON.stringify(result));
@@ -5,7 +5,7 @@ const text=max=>z.string().trim().min(1).max(max);
5
5
  const description='Questions for your user. Ask here, not in chat: the question shows as a card in the Operator queue, your user answers there, and the answer comes back to you as a "Read your user’s answer" task in your queue. An answer never grants send authority.';
6
6
  export const questionOperations={
7
7
  agent_question_ask:{schema:z.object({workspaceId,question:text(1000),context:z.string().trim().max(2000).optional(),options:z.array(text(120)).max(6).optional(),campaignId:uuid.optional(),taskId:uuid.optional(),topic:text(64).optional()}).strict(),method:'POST',path:a=>base(a),body:({workspaceId:_,...rest})=>rest,scoped:true,description:`${description} Ask one clear question with the context your user needs and up to 6 suggested answers. Link the campaign or queue task it is about. Give a topic (first_campaign, campaign:<id>, tools) so a newer question on the same topic replaces the older one. Tell your user in chat that the question is in Operator. Keep working on what you can while you wait.`},
8
- agent_question_list:{schema:z.object({workspaceId,state:z.enum(['open','answered','withdrawn','superseded','all']).optional(),limit:z.number().int().min(1).max(100).optional()}).strict(),method:'GET',path:a=>{const q=new URLSearchParams();if(a.state)q.set('state',a.state);if(a.limit)q.set('limit',String(a.limit));const s=q.toString();return `${base(a)}${s?`?${s}`:''}`},scoped:true,description:`${description} List questions. Agents see only their own; open questions come oldest first.`},
8
+ agent_question_list:{schema:z.object({workspaceId,state:z.enum(['open','answered','withdrawn','superseded','closed','all']).optional(),limit:z.number().int().min(1).max(100).optional()}).strict(),method:'GET',path:a=>{const q=new URLSearchParams();if(a.state)q.set('state',a.state);if(a.limit)q.set('limit',String(a.limit));const s=q.toString();return `${base(a)}${s?`?${s}`:''}`},scoped:true,description:`${description} List questions. Agents see only their own; open questions come oldest first.`},
9
9
  agent_question_get:{schema:z.object({workspaceId,questionId:uuid}).strict(),method:'GET',path:a=>`${base(a)}/${a.questionId}`,scoped:true,description:`${description} Read one question and its answer. Reading an answer marks it read.`},
10
10
  agent_question_withdraw:{schema:z.object({workspaceId,questionId:uuid}).strict(),method:'POST',path:a=>`${base(a)}/${a.questionId}/withdraw`,body:()=>({}),scoped:true,description:`${description} Withdraw an open question you asked, for example once you found the answer yourself.`},
11
11
  agent_question_answer:{schema:z.object({workspaceId,questionId:uuid,answer:text(2000)}).strict(),method:'POST',path:a=>`${base(a)}/${a.questionId}/answer`,body:a=>({answer:a.answer}),scoped:true,description:'Answer an agent’s question as the user (user API token only; agents cannot answer). The answer goes back to the agent that asked as a task.'},
@@ -6,12 +6,17 @@ export async function callSenderAccounts(name,args,profilePath){
6
6
  return callOwnerAccountOperation(senderAccountOperations[name],args,profilePath,'sender account');
7
7
  }
8
8
  export async function senderAccountsCli(args){
9
- const flags={},positions=[];const known=['profile','file','cursor','limit','aliases-cursor','aliases-limit','history-cursor','history-limit','action-type','capability','provider','account-ref'];
9
+ const flags={},positions=[];const known=['profile','file','cursor','limit','aliases-cursor','aliases-limit','history-cursor','history-limit','action-type','capability','provider','account-ref','workspace','plan'];
10
10
  for(let i=0;i<args.length;i++){const arg=args[i];if(arg==='--json')continue;if(!arg.startsWith('--')){positions.push(arg);continue;}const key=arg.slice(2);if(!known.includes(key)||Object.hasOwn(flags,key)||!args[i+1]||args[i+1].startsWith('--'))throw Error('Unknown, repeated or incomplete sender option.');flags[key]=args[++i];}
11
11
  if(positions[0]==='sender'&&positions[1]==='managed'){
12
+ if(positions[2]==='plan'){
13
+ const plans=['free','premium_career','premium_business','premium_all_in_one','sales_navigator_core','sales_navigator_advanced','recruiter_lite'],usage=`Use sender managed plan MANAGED_ACCOUNT_ID --plan ${plans.join('|')}|none [--workspace ID] [--profile PATH] (the user only; none clears it).`;
14
+ if(positions.length!==4||Object.keys(flags).some(key=>!['profile','plan','workspace'].includes(key))||!flags.plan||![...plans,'none'].includes(flags.plan))throw Error(flags.plan&&![...plans,'none'].includes(flags.plan)?`${flags.plan} is not a plan. ${usage}`:usage);
15
+ console.log(JSON.stringify(await callWorkspace('workspace_managed_account_plan_set',{workspaceId:flags.workspace,managedAccountId:positions[3],plan:flags.plan==='none'?null:flags.plan},configuration(flags.profile??process.env.AUDIENTI_PROFILE))));return;
16
+ }
12
17
  const names={list:'workspace_managed_accounts_list',connect:'workspace_managed_account_connect'};
13
- if(!names[positions[2]]||positions.length!==3||Object.keys(flags).some(key=>key!=='profile'))throw Error('Use sender managed list|connect [--profile PATH].');
14
- console.log(JSON.stringify(await callWorkspace(names[positions[2]],{},configuration(flags.profile??process.env.AUDIENTI_PROFILE))));return;
18
+ if(!names[positions[2]]||positions.length!==3||Object.keys(flags).some(key=>!['profile','workspace'].includes(key)))throw Error('Use sender managed list|connect|plan [--workspace ID] [--profile PATH].');
19
+ console.log(JSON.stringify(await callWorkspace(names[positions[2]],{workspaceId:flags.workspace},configuration(flags.profile??process.env.AUDIENTI_PROFILE))));return;
15
20
  }
16
21
  if(positions[0]!=='sender'||positions[1]!=='accounts')throw Error('Use sender accounts list|get|alias|register|bind|unbind|state|automation|modes.');
17
22
  const automation=positions[2]==='automation',modes=positions[2]==='modes',activity=positions[2]==='activity',nested=automation||modes||activity,command=nested?`${positions[2]}-${positions[3]}`:positions[2];
@@ -31,6 +31,7 @@ export const workspaceExecutionOperations={
31
31
  // LinkedIn accounts connected through Audienti. Only the user connects one; agents list them and send through work send.
32
32
  export const managedAccountOperations={
33
33
  workspace_managed_accounts_list:{schema:z.object({workspaceId:uuid.optional()}).strict(),scoped:true,method:'GET',path:a=>`/workspaces/${a.workspaceId}/managed-accounts`,description:'List the LinkedIn accounts connected through Audienti in this workspace, with their sending account IDs and status.'},
34
+ workspace_managed_account_plan_set:{schema:z.object({workspaceId:uuid.optional(),managedAccountId:uuid,plan:z.enum(['free','premium_career','premium_business','premium_all_in_one','sales_navigator_core','sales_navigator_advanced','recruiter_lite']).nullable()}).strict(),scoped:true,method:'PUT',path:a=>`/workspaces/${a.workspaceId}/managed-accounts/${a.managedAccountId}/plan`,body:a=>({plan:a.plan}),description:'The user only, not agents. States the LinkedIn plan of a managed account, which sets how long a connection note may be. Pass plan null to clear it; the safer figures then apply. Returns the account.'},
34
35
  workspace_managed_account_connect:{schema:z.object({workspaceId:uuid.optional()}).strict(),scoped:true,method:'POST',path:a=>`/workspaces/${a.workspaceId}/managed-accounts/connect`,body:()=>({}),description:'The user only, not agents. Returns a sign-in link, valid 15 minutes, where the user connects a LinkedIn account through Audienti.'},
35
36
  };
36
37
  export const narrowExecutionOperations={
@@ -139,7 +139,7 @@ export class InvalidArguments extends Error {
139
139
  }
140
140
  export class WorkspaceRequestError extends Error {
141
141
  constructor(status, code, message, retryAt, blockers, requestTraceId) { super(message); this.status = status; this.code = code; this.retryAt = retryAt; this.blockers = blockers; this.requestTraceId = requestTraceId; }
142
- toJSON() { return { ...(this.requestId ? {requestId:this.requestId,recovery:this.recovery} : {}), ...(this.requestTraceId ? {requestTraceId:this.requestTraceId} : {}), code:this.code, status:this.status, message:this.message, ...(this.currentRevision!==undefined ? {currentRevision:this.currentRevision} : {}), ...(this.retryAt ? {retryAt:this.retryAt} : {}), ...(this.blockers ? {blockers:this.blockers} : {}), ...(this.charged ? {charged:this.charged} : {}) }; }
142
+ toJSON() { return { ...(this.requestId ? {requestId:this.requestId,recovery:this.recovery} : {}), ...(this.requestTraceId ? {requestTraceId:this.requestTraceId} : {}), code:this.code, status:this.status, message:this.message, ...(this.currentRevision!==undefined ? {currentRevision:this.currentRevision} : {}), ...(this.campaigns ? {campaigns:this.campaigns,campaignCount:this.campaignCount} : {}), ...(this.retryAt ? {retryAt:this.retryAt} : {}), ...(this.blockers ? {blockers:this.blockers} : {}), ...(this.errors ? {errors:this.errors} : {}), ...(this.charged ? {charged:this.charged} : {}) }; }
143
143
  }
144
144
  export class HostedResponseUnknown extends Error {
145
145
  constructor(requestId, reason, requestTraceId) { super('The server response is unknown. Paid work may have started. Retry using the same requestId and principal; never generate a new ID to recover this call.'); this.requestId=requestId; this.reason=reason; this.requestTraceId=requestTraceId; }
@@ -161,10 +161,13 @@ export async function callWorkspace(name, args, config) {
161
161
  if(!response.ok) {
162
162
  const code = typeof result?.error?.code==='string' && /^[a-z_]+$/.test(result.error.code) ? result.error.code : 'request_failed';
163
163
  const retryAt = typeof result?.error?.retryAt === 'string' && /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d{3}Z$/.test(result.error.retryAt) ? result.error.retryAt : undefined;
164
- const detail = ((response.status===400 && (code==='invalid_fields' || code==='validation_error' || code==='secret_rejected')) || (response.status===404 && code==='not_found') || (response.status===409 && (code==='revision_conflict' || code==='identity_conflict' || code==='task_not_for_you' || code==='version_conflict' || code==='request_conflict' || code==='campaign_not_ready' || code==='campaign_not_paced'))) && typeof result?.error?.message==='string' && result.error.message.length<=1000 && !/[\u0000-\u001f\u007f]/.test(result.error.message) && !/^Workspace request failed/.test(result.error.message) && !(config.token && result.error.message.includes(config.token)) ? result.error.message.trim() : '';
164
+ const detail = ((response.status===400 && (code==='invalid_fields' || code==='validation_error' || code==='secret_rejected')) || (response.status===404 && code==='not_found') || (response.status===422 && code==='playbook_cannot_work') || (response.status===409 && (code==='revision_conflict' || code==='playbook_in_use' || code==='definition_archived' || code==='playbook_archived' || code==='starter_playbook' || code==='identity_conflict' || code==='task_not_for_you' || code==='version_conflict' || code==='request_conflict' || code==='campaign_not_ready' || code==='campaign_not_paced'))) && typeof result?.error?.message==='string' && result.error.message.length<=(code==='playbook_cannot_work'?2000:1000) && !/[\u0000-\u001f\u007f]/.test(result.error.message) && !/^Workspace request failed/.test(result.error.message) && !(config.token && result.error.message.includes(config.token)) ? result.error.message.trim() : '';
165
165
  const blockers = (code==='execution_blocked' || code==='campaign_not_ready') && Array.isArray(result?.error?.blockers) && result.error.blockers.length<=28 && result.error.blockers.every(v=>typeof v==='string' && /^[a-z_]+$/.test(v)) ? result.error.blockers : undefined;
166
166
  const error = new WorkspaceRequestError(response.status, code, `Workspace request failed: ${code} (HTTP ${response.status}).${detail ? ` ${detail}` : ''}${retryAt ? ` Retry after ${retryAt}.` : ''}`, retryAt, blockers, requestTraceId);
167
+ if(code==='playbook_cannot_work'&&Array.isArray(result?.error?.errors)&&result.error.errors.length<=30)error.errors=result.error.errors.filter(item=>item&&typeof item.id==='string'&&typeof item.message==='string').map(item=>({id:item.id,level:item.level,message:item.message,...(typeof item.stageKey==='string'?{stageKey:item.stageKey}:{}),...(item.fix&&typeof item.fix.label==='string'?{fix:{id:item.fix.id,label:item.fix.label}}:{})}));
167
168
  if(code==='revision_conflict'&&Number.isInteger(result?.error?.currentRevision)&&result.error.currentRevision>=0)error.currentRevision=result.error.currentRevision;
169
+ // A playbook that is in use names the campaigns that use it (#459).
170
+ if(code==='playbook_in_use'&&Array.isArray(result?.error?.campaigns)&&Number.isInteger(result.error.campaignCount)){error.campaigns=result.error.campaigns.slice(0,20).filter(c=>uuid.safeParse(c?.id).success&&typeof c.name==='string'&&c.name.length<=200&&!/[\u0000-\u001f\u007f]/.test(c.name)).map(c=>({id:c.id,name:c.name}));error.campaignCount=result.error.campaignCount;}
168
171
  // What a failed paid check already spent (pico USD), so a caller counting its spend never loses it.
169
172
  if(typeof result?.error?.charged==='string'&&/^\d{1,20}$/.test(result.error.charged))error.charged=result.error.charged;
170
173
  if(isPaidToolOperation(name,input)){error.requestId=input.requestId;error.recovery=response.status>=500?'retry_same_request_id':'inspect_rejection';}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@audienti/cli",
3
- "version": "0.2.0-exora.7",
3
+ "version": "0.2.0-exora.9",
4
4
  "description": "Audienti CLI and MCP client for app.audienti.com",
5
5
  "type": "module",
6
6
  "bin": {
@@ -57,6 +57,15 @@ approved: inbound wins, and you record it. What the user approves is your answer
57
57
  not decided yet.
58
58
  5. Report the task done.
59
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
+
60
69
  The classification records a `replied` event on the person's active campaigns, except
61
70
  for `bounce` and `out_of_office`, which are not replies from the person.
62
71
  An `out_of_office` reply records an auto-reply instead. The playbook's out-of-office rule
@@ -16,9 +16,27 @@
16
16
  | `name` | 1 to 120 characters. Name the strategy, not the campaign. |
17
17
  | `stages` | 2 to 30 stages, in order. |
18
18
  | `goalKey` | The `key` of the goal stage. Not the first stage. |
19
- | `offRamps` | Up to 30 ways out: `{ key, name, reason }`. `reason` is `rejected`, `not_fit`, `nurture`, `unresponsive`, `taken_over`, `bounced`, `skipped` or `other`. "Bad data" and "repair" are not off-ramps. |
19
+ | `offRamps` | Up to 30 ways out: `{ key, name, reason }`. `reason` is `rejected`, `not_fit`, `nurture`, `unresponsive` or `other`. The system sets `taken_over`, `bounced` and `skipped` itself; do not choose them. "Bad data" and "repair" are not off-ramps. |
20
20
  | `voice` | How messages should sound, in the user's style. |
21
- | `rules` | Optional: `flagOverdue`, `allowBackward` (booleans), `emailWindow` (`weekdays_morning`, `weekdays`, `any_day`), `nurtureDays` (60, 90, 180 or null). Approval of a move is a setting on the account, not a playbook rule. An `autoMove` in an older playbook is ignored. |
21
+ | `rules` | Optional: `allowBackward` (boolean, default false): let a move go to an earlier stage. Approval of a move is a setting on the account, not a playbook rule. |
22
+
23
+ ## The Playbook check
24
+
25
+ Every read and save of a playbook returns `warnings`, a deterministic check that needs no agent. An item
26
+ is `{ id, level, message, stageKey, stepKey, fix }`. A save is refused with `playbook_cannot_work` (422,
27
+ the list in `errors`) when `offRamps` is empty or a stage has `on: []` and nothing sends people to it.
28
+ The other items are warnings; read them after each save and clear them:
29
+
30
+ | Item `id` | Meaning and default fix |
31
+ | --- | --- |
32
+ | `stage_never_ends` | A stage with steps and `completion` `stay`. Use `off_ramp` with the Unresponsive off-ramp. |
33
+ | `stage_by_hand_only` | Nothing opens a stage but a person or agent. Give it an `on` event or finish the stage before it with `next`. |
34
+ | `send_without_wait` | A send step with no `wait`, followed by another send on the same channel. Add a wait. |
35
+ | `goal_shorter_than_waits` | `goalDays` is below the waits added up. Use the waits plus 2 days. |
36
+ | `send_in_person_stage` | A send step in the goal stage or an `Assignee` stage writes to someone who already answered. Remove it. |
37
+ | `steps_after_break_up` | A step after the break-up email. Put it before the break-up step. |
38
+ | `message_without_connection` | A LinkedIn message with condition `Always` and no connection request before it. Use `Only if connected`. |
39
+ | `stages_loop` | Routes or rules send people between stages in a circle. |
22
40
 
23
41
  ## Stage
24
42
 
@@ -29,9 +47,8 @@
29
47
  | `movesInWhen` | What moves a prospect into this stage, for example "They replied". Shown to people; `on` decides. |
30
48
  | `on` | The events that open this stage: `accept`, `reply`, `meeting` or `manual`. Defaults to `["manual"]`. An event moves the prospect to the furthest later stage that opens on it; the stages passed over are recorded as skipped, never as done. |
31
49
  | `owner` | `Agent` or `Assignee` (the person assigned to the prospect). |
32
- | `goalDays` | 0 to 365. How long a prospect should stay before it is overdue. |
33
- | `optional` | Only an optional stage can be disabled. |
34
- | `enabled` | Defaults to true. |
50
+ | `goalDays` | 0 to 365. The days after which a prospect in this stage is overdue (the pipeline card turns orange). 0 means never overdue. |
51
+ | `enabled` | Defaults to true. `false` skips the stage: no one is moved into it. The first stage and the goal stage cannot be skipped. |
35
52
  | `steps` | Up to 20 steps. |
36
53
  | `completion` | `{ "kind": "next" }`, `{ "kind": "stay" }` or `{ "kind": "off_ramp", "offRampKey": KEY }`. The last enabled stage cannot use `next`. |
37
54
 
@@ -66,7 +83,7 @@ Use the same UUID wherever the same placeholder letter appears.
66
83
  "name": "Founder-led outbound",
67
84
  "stages": [
68
85
  { "key": "<A>", "name": "Ready", "owner": "Agent", "goalDays": 3, "steps": [], "completion": { "kind": "next" } },
69
- { "key": "<B>", "name": "Warming", "owner": "Agent", "goalDays": 7, "optional": true, "steps": [
86
+ { "key": "<B>", "name": "Warming", "owner": "Agent", "goalDays": 7, "steps": [
70
87
  { "key": "<uuid>", "channel": "LinkedIn", "action": "View profile", "condition": "Always", "instructions": "Read their recent activity for a signal to mention." },
71
88
  { "key": "<uuid>", "channel": "LinkedIn", "action": "Like a recent post", "condition": "Always", "instructions": "Only a post about their work.", "wait": { "until": "timeout", "days": 3 } }
72
89
  ], "completion": { "kind": "next" } },
@@ -85,7 +102,7 @@ Use the same UUID wherever the same placeholder letter appears.
85
102
  "goalKey": "<F>",
86
103
  "offRamps": [{ "key": "<R>", "name": "Unresponsive", "reason": "unresponsive" }],
87
104
  "voice": "Write like a founder. Lead with their signal, offer something useful, and ask one short question.",
88
- "rules": { "flagOverdue": true, "allowBackward": false, "emailWindow": "weekdays_morning", "nurtureDays": 90 }
105
+ "rules": { "allowBackward": false }
89
106
  }
90
107
  }
91
108
  ```
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "repository": "https://github.com/omalab/exora",
3
- "sourceCommit": "fd3e216215ce86b076aaa883754b54112519f45a",
3
+ "sourceCommit": "0172c74b935d515071798bf8a0a2200ed1369a49",
4
4
  "files": {
5
5
  "LICENSE": "220f297cfcd5c38eb64e6b4b4a77cab4ebc7f4686d4d99c6260a475d02ca11e5",
6
6
  "README.md": "7ff32cc8ac89dd54b64d7b39c0b41b1f37e154b7ea10ad5a7bceefc3df77d87f",
@@ -12,18 +12,18 @@
12
12
  "cli/agents.mjs": "8d0d79ceb2308cc0382e17025b5d8bc3b3c4e1b16215f309134635f19cd2fb84",
13
13
  "cli/assignment-operations.mjs": "fc74e3fd79975cfcbac14891d5dec5f25e662777050fc46a8ad3aae5cf361c59",
14
14
  "cli/assignment.mjs": "90cb85e0df8aadca2ff8391c99e778b41261183b5f38e8c6c32afee2f98f871a",
15
- "cli/attachment-operations.mjs": "f8af683d0f351554e545c4e550870d2925204f896ecf62720b8013d74799bf67",
15
+ "cli/attachment-operations.mjs": "dd29a50d073ded71ebd606fb3a9181bebcf2a1390f75f99cdcb61d84261d102a",
16
16
  "cli/attachments.mjs": "2fee6b4abbf82da9847d4a927a3d4d3ca947a198a903a78927596a2ea858cbf4",
17
17
  "cli/audience-operations.mjs": "133b72dc6b9e3a6b4f173ce28f1524163f9c7b049505b885115f83b9e4af7170",
18
18
  "cli/audiences.mjs": "3c377346efc1a8bdde432a16c2d19a3f51f34f3333d344d3a2bfa94c188a017d",
19
- "cli/audienti.mjs": "8b001322ba573f148a4fef3ee0918ee8a974bbedaf7a0fb497d53055729b5280",
19
+ "cli/audienti.mjs": "4b4bc724d64f3f88a37717476226f6cd5799a8fcd06ee31edb2b4bb56dbe4cc5",
20
20
  "cli/audit-operations.mjs": "184eb58bc479225314b77d6019a185670cd843895a10d9bf58a28de7f1355d91",
21
21
  "cli/audit.mjs": "a7becaed67c58b063e668b5e66844b6dc15c11a63b428faa171b542ad922c001",
22
22
  "cli/budget-operations.mjs": "d74916551ef393e29734c6e13cf9021251e0ed126eeaa529e18cf2332146e1e0",
23
23
  "cli/budgets.mjs": "683e06cb4d410c851286fbd1cdd4fb9f1902726c60fd4fcddb2c13d164786525",
24
24
  "cli/campaign-find.mjs": "9c0a44c69ab7ad3e24a0a2edc97b8f3a14f602e3b6caf7e1943b8e1b93609c4a",
25
25
  "cli/campaign-operations.mjs": "c1ac42f53f8bf19957fac41cc2a80ab4e26a743a7c1cc0b42c8c0e85fcf6e61b",
26
- "cli/campaigns.mjs": "63a65f3af9964ede1a62a5946c9d751110aa29b69cec0a67a451241e73ad8c1d",
26
+ "cli/campaigns.mjs": "a4c1e824b729cdaf1838db68785e62c55d4ba7d57c59d3f71666d2197be2fdf8",
27
27
  "cli/cohort-operations.mjs": "91b1d1fc48e75581bb315f42d735ef94e21687dc216f352337d8a5d180bb691e",
28
28
  "cli/cohorts.mjs": "c71940903430f3e6834608f113534910b75a98b3e410a5942f04051607488485",
29
29
  "cli/connection-registry-operations.mjs": "888ea440d00ffc9ed68d4cc084ff73a5a00d0e60bd6293baba8bb4539a25ae62",
@@ -46,13 +46,13 @@
46
46
  "cli/outcome-operations.mjs": "25380db262c316ea1b34a5ae95abd01f2c8e92b7c877ebe3f0b18ccd5032a7fa",
47
47
  "cli/outcomes.mjs": "e547aa889c3d356c66d122d8fe4fd8f4ff46b18531c1318ca6f238af102cdde9",
48
48
  "cli/owner-account-client.mjs": "beb9736ac84a7ab57220ac1f1ea089b6401cf92fcf7c549417014d285f2340dc",
49
- "cli/playbook-help.mjs": "6e1333d5dd3efc8b4fd85825f51339db198594ebe29dfb95777e969aa84a9459",
50
- "cli/playbook-operations.mjs": "a60e24cbf784a150934cea2dcf36eac06b69def31d4b3151050bf222f3961cff",
49
+ "cli/playbook-help.mjs": "3db800db807bf21fc8a0ab1bbb0c4af3a387cc6be8f68e413b135f714b6c100e",
50
+ "cli/playbook-operations.mjs": "288242344298b74dfff02ac46c49c6005562067e9cc4e281a322d0d1aba04d65",
51
51
  "cli/profiles.mjs": "45929b224ffc916b6dbd7a94e06ac09a1ff7feec430633b10d160c1608fc2d32",
52
- "cli/progress.mjs": "9350bf3d03ffd18bf5493b5bb012841b57624eae96d80aeb2e9088bf1efec5af",
52
+ "cli/progress.mjs": "854da7488db6bfcf53da5edbfaa95cd51c565df87b0df590dc4bad69eaccb104",
53
53
  "cli/prospecting-settings-operations.mjs": "37b881a867c19d9e5f2d02316038284ea0cf09de4f37b6dee2370a1faedf245e",
54
54
  "cli/prospecting-settings.mjs": "b09f47c84416d9892d7d5b17dcf684cbb560e1cacb25513826ff04ef9307626e",
55
- "cli/question-operations.mjs": "5d7a885f2d4e0d2ce2249aadce2b4478b95843d21761f25eabdfe2759582a3b6",
55
+ "cli/question-operations.mjs": "c7070dd1b5ec9c5b0df93012ef08d0d6b4d6e5c8da267a37beb1acc5c3cf4ea1",
56
56
  "cli/questions.mjs": "32bbe0b785b1e6438435faf4697332ea2da3133d4c896720d39377c35e9b2127",
57
57
  "cli/queue-operations.mjs": "bc5f5b8f79f056f579c2d3965d143b45ded4665629e2c4d427adfbaf7e9687fb",
58
58
  "cli/queue.mjs": "046f4dba92ac9eacbb1f12215ab2b49623bae597342da6b415f7afb3c3a1344e",
@@ -64,7 +64,7 @@
64
64
  "cli/secret-operations.mjs": "faefe9c9ece81087dc7aab32e88f331ed942d0757ce5a978b5f0722d514f377f",
65
65
  "cli/secrets.mjs": "cab4ea932e724ce3c807c7825869810b44fb8d75cdacec32f8101353f0e7c5f5",
66
66
  "cli/sender-account-operations.mjs": "97d7a9236c2b72e9863d3d75ab93671348ff97a07d627a3e72350a17623b4d4d",
67
- "cli/sender-accounts.mjs": "0c9be3e441aebf4bea2d2674ee54c4b17ed246c7a8476bcb4debbdad7cc1ec10",
67
+ "cli/sender-accounts.mjs": "406abd62a489894bec2cb3c9ebcc1b19572ab8f2d0fab4854deb78ea7dd89d0a",
68
68
  "cli/skills-mcp.mjs": "6a91b16a789f50035ae0fe33f8ae2758bb9829949db7efb05c6d65dde335a9e0",
69
69
  "cli/skills.mjs": "ea8dfef32664714f9c63251d9a6bbdb44d77d41e272df46bb102ebfcd8df8b66",
70
70
  "cli/suppression-operations.mjs": "d1f827a785670fc6e44d87b18e4cfb6596b09e5be7c4f84c4b9a75ed13a962c6",
@@ -73,14 +73,14 @@
73
73
  "cli/tools.mjs": "6d9c7f27cc50e5e2fc0859c9c2d62bc3e77b5a46d0e2aeaa6cf31e2867138758",
74
74
  "cli/usage-operations.mjs": "64dd1ae850f52bb72c7648be78a8ab2dfab986afba2d2d2cd08034c4f7b3ace6",
75
75
  "cli/usage.mjs": "cd582c061f83ca4f220ddd3cd2a174a1a3a3c0ee308b3d5a5f47752a501e322e",
76
- "cli/work-execution-operations.mjs": "0313dacfd4ba7dcfb30d7a2f6ee016dd77b141de35bff999ed28393e2e86069a",
76
+ "cli/work-execution-operations.mjs": "842d706b36bb035aa54e1dee8bf950f0619c495bcae84faa1926fbb05f6842bc",
77
77
  "cli/work-execution.mjs": "4b74923417eaa70dee0ba8806b64d0acf7c884fb77b76d5e86d567cf92e51b01",
78
78
  "cli/work-feedback-operations.mjs": "a998f2b2dbe38ceb2bad74f7fa61c31f87f842755449248b7eb0c04185c0ee3d",
79
79
  "cli/work-feedback.mjs": "f3b77ecfd7ea1aba9c9f03c9d894137547e9c5575d9daa359ed94505ad025852",
80
80
  "cli/work-operations.mjs": "8b3a84ca15e8e4ad79ef92ced4c5adaffd5129f630e3bfc2c74a844a90867600",
81
81
  "cli/work.mjs": "751107ecc3de5a6be9db4a068cddc41f3ad8e77c87e59686eaf522862caf9e9f",
82
- "cli/workspaces.mjs": "fc44a0df664f79d3e6fb84d0837f30ae20f01f0951f59e5e99ce116f6e76feef",
83
- "package.json": "16d565964cf7d4a5f8d6c45bafa1e6d68888c9f4f2a389bbd507ffdb2546e0fa",
82
+ "cli/workspaces.mjs": "fc77d4a4b870c7ea97f67a170c235833631b8f0dd5203ad0d5df6de46794c937",
83
+ "package.json": "b744e36b43f33418222348ba541638167edcd69b1bb4458a828a160bec9d1074",
84
84
  "skills/agent-rooms/SKILL.md": "08f917ff85fee1d23499a4499056ce845b39ce6727029b104819dad41ee87cc2",
85
85
  "skills/agent-rooms/references/commands.md": "ef5afa5543ca9da967a147f638774647998ea1c1a2a821959f3bf87d54c292bd",
86
86
  "skills/audienti/SKILL.md": "8f2124065bfc3a005b44e194e7dcce9bbf14062ab332e7db66d441eeccb518ed",
@@ -96,9 +96,9 @@
96
96
  "skills/getting-started/references/capabilities.md": "b41ab2ec511db127c8e43c80c71aa4fd516ca816af393f5a1c7b3456e355af96",
97
97
  "skills/getting-started/references/mailbox-check.md": "113574bb5a573125620dcde4e65ebf44164f7b0727cc936fb41234f5d57bba73",
98
98
  "skills/getting-started/references/outcome-checks.md": "53919f2c2adf2bd24e602f4c2a21fbd47c807b115b2a1f9bb7893ebf48dc715a",
99
- "skills/getting-started/references/replies.md": "edb9c0d64749f0fe86c3e054b438fcb2248a0ef6c666b95e470a16e2f5dce275",
99
+ "skills/getting-started/references/replies.md": "865c97bea9c4bbecc2aba15be933356d11c37f3239be2cd436442230b5e0501e",
100
100
  "skills/getting-started/references/reply-checks.md": "85fe692ad6b7bc73e204de313b27d225be3da1a0af71e8e7bb9945ae511a0dc3",
101
101
  "skills/playbook-design/SKILL.md": "551f10bd07541056521950930a5d8eaa332f5e9d742d157d7d0e01beff6c4460",
102
- "skills/playbook-design/references/fields.md": "0082c96c1059bf4204b51fe625c03a130e141e4bf301e3205f06a544f6419b69"
102
+ "skills/playbook-design/references/fields.md": "0e5be5926b49b225046c4fdbf5c2df9e309bd58da46cc59a01e18aa7daaabc4e"
103
103
  }
104
104
  }