Sliq
Server Details
Run LinkedIn and email outbound from Claude, ChatGPT, or any AI agent.
The last two health checks of this connector failed. If this server requires authentication, missing or expired test credentials may be the cause. A test profile lets Glama authenticate for health checks and discover tools; it is separate from your personal connections.
If you are the author, claim ownership, then add or update a test profile under Admin → Test Profile.
- Status
- Unhealthy
- Uptime
- 45.5% over the last 24 hours
- OAuth
- Works in Glama
- Last Tested
- Transport
- Streamable HTTP · MCP 2025-11-25
- URL
- ChatGPT
- –
- Claude
- –
TDQS
Scored across 177 tools
Most tools have a genuinely distinct target (agents, triggers, prospects, tags, gifts, queues), and the descriptions work hard to draw boundaries between near-siblings (query_people vs list_people vs query_search_results, query_linkedin_posts vs query_monitored_posts, draft_email vs queue_email_outreach, update_prospect vs correct_reply_outcome vs move_prospect_to_node). But at 177 tools there are many overlapping readers and several near-duplicate pairs (list_*/query_* over the same entities, classify_*/estimate_* variants), so selection still requires reading long descriptions rather than recognizing a name.
The dominant pattern is consistent snake_case verb_noun (add_trigger, create_deal, query_prospects, update_prospect, delete_segment_tag, estimate_*_scope), with clear families for list/get/query/create/update/delete/queue/classify/correct. Minor deviations exist in vendor-prefixed and noun-first names (apollo_enrich, exa_search_news, theirstack_search, google_news_search, matches_icp, start_find_search), which slightly break the verb-first convention but remain readable.
177 tools is far beyond a coherent, navigable surface for any single server; it is a whole product's UI flattened into one namespace. Even though the domain is broad (outreach, CRM, agents, monitoring, analytics, gifting, Instantly/Luma integrations), the count forces an agent to triage dozens of candidates per request and guarantees redundancy.
Coverage is unusually deep: full lifecycle for agents and triggers, campaigns and flow nodes, prospects, CRM deals/tasks, tag groups with classify/correct/estimate, email and LinkedIn queues, gifts, monitoring, analytics and settings all have create/read/update/delete paths. Gaps are minor and workaroundable — there is no direct delete for canonical people/company records (only skip/archive semantics), and email sending is only reachable via draft+send or queueing.
Available Tools
177 toolsadd_triggerAdd triggerAInspect
You MUST call get_skill_guide('trigger_code') before writing the trigger's
code — the qualification rules, sandbox globals, tool surface, and out
contract live in the skill. New code runs on the trigger's next firing.
To add a flow node's on_enter enactment — a terminal hook, or re-arming a
node whose row was removed — pass an on_enter trigger with the target
node_id (any node kind; the node must already exist in the sequence). One
on_enter per node — if it already has one, edit that with update_trigger
instead of adding a second. On a send or connection-request node, that trigger's prompt
holds only extra instructions; the message or note itself goes in the node's
message_templates (update_node).
Dict with success, agent_id, trigger_id, next_run_at, and the added trigger's
full dict under trigger, plus a warning when the agent is paused, since its
triggers don't fire until it is set back to active. Read the agent's
other triggers via get_agent_details.
Inputs: trigger (required), agent_id (required), on_behalf_of.
apollo_enrichApollo enrichARead-onlyInspect
Pass all candidate domains at once — one call with the full list,
not one call per domain. The tool fans them out to Apollo's
/organizations/enrich endpoint in parallel (ThreadPoolExecutor,
max 20 concurrent, well within Apollo's 1000 req/min limit), so a
batch costs one tool round instead of N.
Org-only (company domains, not people). For person enrichment
(work email, LinkedIn URL), use find_email instead.
Costs 1 Sliq credit per found=True result (free for found=False,
free on BYO Apollo). Soft-fails on insufficient credits — already-
enriched results are still returned.
Per-user 24h cache: any domain this user already enriched — in an earlier run_code sandbox, an earlier call, or an earlier run — is served from cache with no second Apollo call and no second charge (both found=True and found=False are cached). The input list is also deduped by canonical form before fan-out.
Each enriched company is also saved to your canonical company entities, queryable via query_companies. List of dicts, one per input domain, in the same order. Each is either: {found: True, name, domain, industry, employee_count, annual_revenue, founded_year, latest_funding_stage, total_funding, location, linkedin_url, description} OR: {found: False, queried_domain, message}
latest_funding_stage is Apollo's title-case display string
(e.g. "Seed", "Series A", "Series B", ..., "Series Unknown",
"Private Equity", "Post-IPO Equity", "Other") — passed through
verbatim, NOT a lowercase_underscore enum. It is None/absent
when Apollo has no funding data for the org. total_funding
is a USD integer (Apollo defaults it to 0, not null, when
absent). Apollo's funding coverage is patchy, especially for
bootstrapped / PE-owned / privately-held companies. Callers
gating on these fields should decide how to handle missing
data — matches_icp includes-on-null rather than
excludes, so legit rows with thin Apollo coverage aren't silently
dropped, and normalizes case + separators when comparing the
stage string.
Inputs: domains (required), on_behalf_of.
apollo_saveApollo saveADestructiveInspect
Save people as contacts or companies as accounts in the user's Apollo CRM, where they show in Apollo's web UI. Confirm with the user before calling this tool.
Each contact needs an email; it's the only required field. For a contact whose
email you don't have, get it with find_email first, passing an apollo_search
result's name and apollo_id, or the person's full name and company. Then save
the real name and email it returns as {first_name, last_name, email, title,
organization_name}.
For accounts, the result splits the count into "created" (new rows) and "existing" (already in the CRM — Apollo dedupes by domain); "saved" is their sum, so a re-save reads as saved == requested rather than a false failure.
Inputs: type (required), items (required), sequence_id, on_behalf_of.
apollo_searchApollo searchARead-onlyInspect
Search Apollo's database for people or companies.
People search is free. Organization search costs 1 Sliq credit per page when the user is on Sliq credits; free when the user has connected their own Apollo key. Use liberally for people; be deliberate for organizations.
People search returns each person's first name plus a masked last name,
and no LinkedIn URLs. The has_email and has_phone fields are
boolean indicators only — the actual email and phone number are
not returned. These masked previews are NOT saved to the Output tab
(no identity to key or link them by) and should not be recorded via
record_search_results. To turn a preview into an actionable, saved
person, call find_email with the result's name and apollo_id — it
resolves the real person and saves them to the user's people (what
query_people reads), with the email it found when they had none. It
doesn't add them to an agent's list: when they belong on one, record
them with record_search_results, keyed by the linkedin_url
find_email returns (its email when there is none).
Inputs: type (required), page, keywords, locations, +9 more.
append_learningAppend learningAInspect
BEFORE COMPOSING YOUR BATCH, filter each candidate entry. A learning is ONLY:
A reusable rule about how the agent should run, that applies to every future firing, not just this one.
A learning is NOT (these are the most common mistakes — produce zero of these):
Per-entity / per-contact facts. Anything naming a specific company, person, deal, or URL. Examples that DO NOT belong here: "Heitman LLC (heitman.com) is a real estate firm in Chicago", "Jamestown Management hired a new CFO", "Alice Chen's first name is 'Alice'", "ExampleCo: 200 waitlist, 30 live users". → Use
track_prospects(agent_id, items=[...])(ortrack_monitored_companiesfor a monitored company) for per-entity lifecycle tracking, OR →update_workspace(section='outputs', key='<your_list>', value=[...])to store as an agent deliverable.Per-run summaries. Anything describing what happened on a specific date or run. Examples: "Follow-up run 2026-04-27: 0 people in 'messaged' stage", "Apr 23 scan: same source returned, no new angles". These are auto-saved to the agent's run history every run — don't duplicate them here.
General user preferences. "User wants concise emails", "user prefers Tuesday meetings". → Use
save_memory.
Valid learnings look like rules-of-thumb, not observations. They're short prose, no proper nouns, no dates, no per-entity data. Good examples:
"PostHog returns 1-day data, not an error — if the query returns one row it's complete, not partial."
"Internal team syncs rarely produce postable ideas — skip quickly to save Tavily budget."
"r/SaaS posts about LinkedIn automation pain are strong leads."
"Tavily score < 0.09 against generic signal_keyword queries is reliably tangential — keep min_score=0.09."
If you cannot rewrite your candidate entry into a rule-of-thumb
shape without naming a specific entity, date, or run number, it
doesn't belong here — store it via track_prospects or
update_workspace instead.
USAGE
Pass learnings=["...", "..."] to add one or more new entries in one
call. The whole batch is atomic — either all entries are appended or
none are (see "Cap behavior" below). Singleton case is
learnings=["..."]. Batching is strictly preferred over multiple
single-entry calls: one round-trip per run is dramatically cheaper
in latency and tokens than one round-trip per insight.
Pass replace_with=[...] to atomically replace the full list — use
this when the prior call returned a "cap reached" ModelRetry asking
you to consolidate.
Scope: per-agent and persistent. Distinct from save_memory (per-user
preferences).
CAP BEHAVIOR
The list is capped at 25 items. If a batch would push the list over
the cap (current_count + batch_size > 25), the WHOLE batch is
rejected via ModelRetry asking you to consolidate first via
replace_with. After consolidating, re-submit your full batch — no
need to track which entries "landed" since none were stored on the
rejected call. Consolidate by DROPPING entries that match the
anti-examples above (per-entity facts, per-run summaries), not by
reshuffling — the cap is a forcing function for hygiene, not a
length limit on the same content.
Dict with success, agent_id, and the new learnings count.
Inputs: agent_id (required), learnings, on_behalf_of, replace_with.
attach_to_outreachAttach to outreachAInspect
Only attach when the user explicitly asks to attach a specific file to someone's
message — never on your own initiative. attachment_id comes from list_attachments.
Name the queued message by exactly one handle: recipient_email for a queued email
(pending or awaiting approval); target_provider_id for a queued LinkedIn message, or
target_identifier for one whose profile is still resolving — both as shown on the row
in manage_linkedin_invite_queue's status read. On LinkedIn only messages carry files,
not connection requests or InMails, and LinkedIn takes fewer file types than email; a
file the channel can't send is refused with the reason. The same file can be attached
to several messages.
The attached file's name and every file now on that message, shaped
{'attached': True, 'filename', 'attachments': [{'id', 'filename', 'mime_type', 'size'}]}.
Inputs: attachment_id (required), on_behalf_of, recipient_email, target_identifier, +1 more.
cancel_giftCancel giftADestructiveInspect
Inputs: gift_id (required), agent_id (required), on_behalf_of.
check_email_daily_usageCheck email daily usageARead-onlyInspect
Call this before queuing bulk email outreach — the mailboxes list
tells you which mailboxes still have headroom today. Each connected
mailbox has its own daily cap (Gmail/Outlook score reputation per
from address, not per Sliq user). An empty mailboxes list means no
email account is connected — tell the user to connect a Gmail or Outlook
mailbox in Settings. max_daily_sends is the mailbox's user-set cap,
which the user changes on the Integrations page; daily_limit is today's
effective cap, which is lower while the mailbox is still warming up.
{
'emails': {
'mailboxes': [
{'email': str, 'provider': str, 'sent_today': int,
'daily_limit': int, 'max_daily_sends': int,
'remaining_today': int, 'is_default': bool},
...
],
},
'queue': {
'pending': int, 'pending_approval': int,
'sent_total': int, 'failed': int,
'next_send': str, # optional
},
}
Inputs: on_behalf_of.
check_linkedin_daily_usageCheck LinkedIn daily usageARead-onlyInspect
Call this before sending multiple LinkedIn messages, InMails, or connection
requests to get a fresh read of today's headroom (limits, remaining,
ramp-up) plus the current queue counts — re-call it after queueing or
approving sends, which leave the in-context figures out of date. For the
per-contact queue (names, scheduled times, messages), use
manage_linkedin_invite_queue with action='status'.
Dict with usage counts, limits, queue counts, and configurable_limits
— per action the standard default, the tier-aware ceiling (the max the
user can raise it to), and the user's current custom cap (None = using
the default). Quote ceiling when the user asks how high they can set a cap.
Inputs: on_behalf_of.
classify_message_tag_groupClassify message tag groupAInspect
Classification costs credits (a flat per-message rate). The return carries the count of messages the run will process and the credits it will charge (exact — the same prospect-resolved population the run bills), so you can tell the user the cost; a run never charges more than the balance. If the balance can't cover the estimate the run is not enqueued — the return says so, and you should relay it. Dict with the in-scope message count and estimated credit cost, plus whether the run was enqueued or blocked for insufficient credits. Enqueued -> {"enqueued": true, "group_id", "message_count", "estimated_credits"}. Blocked -> {"enqueued": false, "reason": "insufficient_credits", "group_id", "message_count", "estimated_credits", "balance"}.
Inputs: group_id (required), force, channel, agent_ids, +2 more.
classify_segment_groupClassify segment groupAInspect
Classification costs credits (a flat per-person rate). The return carries the count of people the run will process and the credits it will charge (exact — the same population the run bills), so you can tell the user the cost; a run never charges more than the balance. If the balance can't cover the estimate the run is not enqueued — the return says so, and you should relay it. Dict with the in-scope person count and estimated credit cost, plus whether the run was enqueued or blocked for insufficient credits. Enqueued -> {"enqueued": true, "group_id", "person_count", "estimated_credits"}. Blocked -> {"enqueued": false, "reason": "insufficient_credits", "group_id", "person_count", "estimated_credits", "balance"}.
Inputs: group_id (required), force, on_behalf_of.
correct_message_classificationCorrect message classificationAInspect
Inputs: source (required), tag_ids (required), group_id (required), source_id (required), +1 more.
correct_reply_outcomeCorrect reply outcomeAInspect
Inputs: channel (required), outcome (required), message_id (required), on_behalf_of.
correct_segment_classificationCorrect segment classificationAInspect
Inputs: tag_ids (required), group_id (required), person_id (required), on_behalf_of.
create_agentCreate agentAInspect
Create a background agent that runs automatically based on triggers.
Do NOT create a new agent for follow-ups, event handlers, or scheduled checks
that continue an existing agent's work — use add_trigger on that agent instead.
When the agent matches one of the built-in agent templates — the template_id
enum lists them — set its template_id and seed workspace.inputs per that
agent's skill guide. The template_id runs the template's setup; seed a
template's inputs without it and the agent is not fully wired up. If no template
fits, omit template_id and the agent runs as a general background agent.
You MUST call get_skill_guide('trigger_code') before writing any trigger's
code — the qualification rules, sandbox globals, tool surface, and out
contract live in the skill. code runs on the trigger's next firing.
Inputs: goal (required), title (required), triggers (required), workspace, +2 more.
create_calendar_eventCreate calendar eventADestructiveInspect
Use search_emails or search_calendar to find email addresses for prior attendees. For new attendees whose email you don't have, use find_email. Dict with success status and event details
Inputs: subject (required), end_datetime (required), start_datetime (required), body, +5 more.
create_crm_taskCreate CRM taskAInspect
Inputs: title (required), notes, deal_id, due_date, +4 more.
create_dealCreate dealAInspect
A deal needs a person or a company (or both). Link someone Sliq already knows by id (query_people / query_companies), or pass new_person / new_company to add them. If the person already has an open deal this refuses and names it — update that deal instead, unless the user wants a second one (then pass allow_duplicate=true). The created deal, same shape as a query_deals row.
Inputs: name (required), notes, stage, amount, +8 more.
create_message_tag_groupCreate message tag groupAInspect
Inputs: name (required), tags (required), mode, criteria, +2 more.
create_pageCreate pageAInspect
Inputs: code (required), name (required), change_note, description, +1 more.
create_segment_groupCreate segment groupAInspect
Inputs: name (required), tags (required), mode, criteria, +2 more.
define_sequenceDefine sequenceAInspect
Author (or replace) the campaign's flow sequence — a DAG of outreach nodes. Each node
carries its own out-edges as typed fields, so the grammar is the schema: a connection
request has accepted / already_connected / timeout slots, a send has replied /
follow_up, a silent action, a manual step and a gift step each have then, an Instantly
step has replied / no_reply, a decision has arms, a terminal has none.
Derive the DAG from the agent's goal + triggers: one node per step, each natural-language
fork a decision, and terminal only for resting end-states (auto-skip, human handoff,
done). A fork is any rule about who gets a step or which steps they get — a filter (keep
only people in the right location, role or seniority, company or industry, skipping the
rest) or a route (a different path of steps by role, school or sector) — and each is its
own decision node rather than a condition inside a send step's on_enter code or prompt. A
wording difference within one step is a {slot} in its message_templates, not a fork.
The flow must be acyclic, every node reachable from start, and every path capped by a
terminal (only a terminal may dead-end) — all enforced.
The shape:
start(exactly one, the DAG head):{id, label, entry}. Holds prospects no send has reached (label it "Not started").entryis the single first touch —{to}for an immediate open, or{to, after: {value, unit}}(units w/d/h/m) to hold that long from when each prospect is tracked. The flow fires it on its own: do NOT queue the first sends yourself on a sequenced campaign — track prospects and let the flow open.connection_requestnode (LinkedIn):{id, label, kind, accepted, already_connected, timeout?, message_templates?}.message_templatesholds its note (see below); without one the step'son_entercode or instructions decide (its minted default code sends no note). Routes BOTH send outcomes; both required.accepted(invite taken) is an advance like a silent action'sthen—{to}to move on once the accept is detected, or{to, after: {value, unit}}to hold the prospect on the request that long after the accept is detected (detection lags the real accept by up to ~8 hours, so the wait counts from detection).already_connected(a CR onto an existing 1st-degree connection, a no-op) is a target node id. Routealready_connectedto aterminalby default (surface the existing relationship for opt-in rather than cold-messaging it); point it at the message step only when the user says to reconnect with existing connections.timeoutis an optional held advance{delay: {value, unit}, to}off an unaccepted request.sendnode (a repliable send — LinkedInmessage/inmail, or anemail):{id, label, kind, channel, action, replied, follow_up?, message_templates?}.channelislinkedinoremail(so one sequence spans both);actionis a LinkedIn verb for channel linkedin oremailfor channel email. Routesreplied(required) so a reply advances the prospect off the node;follow_upis an optional held no-reply follow-on{delay, to}.message_templatesholds the step's message (see below).actionnode (a bodyless LinkedIn action — follow / resolve / comment / reaction):{id, label, kind: "action", channel: "linkedin", action, then}. Nothing to reply to;thenis its single advance —{to}fires the instant the action's send lands (a bodyless step flows straight into the next node with no hold), or{to, after}to hold first. Aresolveaction may addnotify: true— the visible "View Profile" visit (LinkedIn notifies the person you viewed them), a warm-up touch typically placed later; the default silent resolve just reads profile data to branch on.manualnode (a step a HUMAN performs off-platform that the system can't enact — a call, a handwritten card, a recorded note):{id, label, kind: "manual", action_description, then}. No channel.action_descriptionis the fully-authored ask (no send is composed, no agent run boots — the step is purely a note for the user); entry queues it onto the user's home approval list.thenis the step's single advance once the user marks it done —{to}to move on at once, or{to, after: {value, unit}}to hold that long after it (e.g. a follow-up message a few days after the call). UsemanualONLY when a human must act — never for a step the agent can do itself (that step never happens, since a manual node boots no run): an automatable side-effect (update a CRM, create a task in a connected tool, post a notification) is anautomationnode, and an automatable outreach touch is anaction/sendnode.giftnode (a physical gift Sliq orders online and ships to the prospect's US address — use it whenever the user wants a gift sent as a step):{id, label, kind: "gift", product_url?, max_price_usd?, gift_note?, then}. No channel. Entry queues one gift card per prospect onto the user's approval list, with a short note the enactment run writes for each prospect; nothing is ordered until the user approves the card, where they complete or change the recipient, product, price ceiling and note, and type the shipping address or send the recipient a link to enter their own, which brings the card back for them to approve with that address.product_url(the product page) andmax_price_usd(the most the item may cost including tax and shipping, in whole US dollars) are optional defaults pre-filled on every card, andgift_note(at most 240 characters) is the optional step note each prospect's own note is written from; set the ones the user named and leave the rest out. For a personalized or custom-made item the user names asproduct_url(one that needs text, a logo or a photo added), leave it out, tell them Sliq can't order it, and ask for an item that ships as listed. Each approved gift charges the owner's card for gifts the order's actual total, at mostmax_price_usd, plus about $3.30 + 2.9% in fees on top. Set a budget the user names for each gift asmax_price_usdas is, even one they call all-in, and tell them the fees come on top of it: a lower limit can make the order fail for the item they picked, so lowering it is their call.thenis the gift's single advance once it is delivered —{to}to move on at delivery, or{to, after: {value, unit}}to hold that long after it.instantlynode (hands each prospect to a campaign in the user's connected Instantly workspace, which emails them on that campaign's own sequence and schedule — use it whenever the user wants a Sliq campaign's prospects added to, emailed by, or handed off to an Instantly campaign):{id, label, kind: "instantly", campaign_id, replied, no_reply}. The Instantly campaign must be set to stop emailing a lead who replies, which authoring checks; pass itsidfrominstantly_list_campaignsascampaign_idand itsnameaslabel, which names the campaign wherever the step shows. There is nothing to write: the Instantly campaign holds the emails.repliedis a bare node id, like a send'sreplied.no_replyis a required held advance{delay, to}, like a send'sfollow_up: once Instantly has finished the campaign's sequence, a prospect who hasn't replied moves on whendelayhas passed since Instantly's last email to them, sodelayis how long they get to answer that email. When the user names no wait, use{value: 3, unit: "d"}and tell the user it's 3 business days. A prospect with no stored email address, or a bounced one, is blocked on the step. When Sliq stops pursuing the prospect (they reply elsewhere, are removed from the campaign, or leave the step), it removes the lead from the Instantly campaign while Instantly is still running it with no reply, and leaves a lead the user moved to another campaign or gave an interest status in Instantly.automationnode (an automated non-send side-effect the AGENT performs — update a CRM record, create a HubSpot task, post a Slack/webhook notification, tag a record):{id, label, kind: "automation", then}. No channel.thenis the next node's id as a bare string, not the{to}object a manual, gift or silent action step'sthentakes. Author the side-effect itself onto this node's on_enter trigger withupdate_trigger(aprompt, orcodefor a deterministic one) — the same way a terminal node's hook is authored, not a field here; the enactment run carries it out for each prospect then advances. Unlikemanualit boots an agent run and needs no human; unlike aterminalhook it advances. Reach for this when the user wants a step that "does something" automatically mid-sequence and then continues.decisionnode:{id, label, kind, rule, arms}.ruleis the NL fork judgment;armsis a list of{case, to}— eachcasea non-blank condition, the default authored as one too (e.g.case: "else"). A decision has no timed edge and no bare advance — only arms. A case arm may target any enactable node — a send / action / automation step, another decision, or a manual, gift or Instantly step — or a terminal: the decision run moves each prospect onto its arm, and entering it fires that node's own on_enter enactment on its next run (a send/action composes its send, a nested decision judges its fork, an automation runs its side-effect, a manual step queues the human's task, a gift step queues its gift card, an Instantly step hands them to its campaign), never an inline send. Chain a downstream decision when several arms share one judgment (DAG reuse); use compound arms on one rule ("A and B" / "A and not B" / "else") when one prospect needs several independent conditions judged together; target a manual step to hand a judged arm off to a human.terminalnode:{id, label, kind}. A resting end-state, no out-edges.
Every timed hold — a start after, a connection_request's held accepted or timeout, a
send follow_up, a silent action's or gift step's held then, an Instantly step's
no_reply — is {value, unit}. A d (day) unit counts BUSINESS days —
Mon–Fri, skipping Saturday/Sunday — and is the DEFAULT for a day-count delay: a plain
"follow up in 3 days" (like "3 business days") is {value: 3, unit: "d"}. Reach for cd
(calendar day) ONLY when the user explicitly wants calendar days including weekends.
w/h/m (week/hour/minute) are always calendar. A day-count hold (d, cd, w) rounds
to an opening of the user's sending window: the next step goes out as the window opens on the
day the wait ends, or the following day (business day, for d) when the wait ends past the
middle of that day's window; a send due on a day the window is off waits for the next day it's
on. With a 9am–6pm window, a 1-day wait after a Monday 10am send goes out Tuesday morning;
after a Monday 5pm send, Wednesday morning.
Entering a node fires its enactment: a connection_request / send / action node
composes and enqueues its step, a manual node queues its step onto the user's
approval list, a gift node queues a gift card for each prospect onto that list, an
instantly node hands each prospect to its Instantly campaign, an
automation node runs its authored side-effect and advances, a decision judges its
fork and routes each prospect. Run a second channel alongside the first by chaining it
off a step's held advance (a CR timeout, a send follow_up, or a silent action's held
then) — not a second start entry: a prospect holds one flow position, so start takes
exactly one entry.
A send node's message is its message_templates: a one-entry list holding the message itself, with each
personalized part a {slot} — a named field ({first_name}, {company}) or a described
slot ({a line about their recent post}); the enactment run fills every {slot} per
prospect and, unless the step's prompt lets it rephrase, keeps the rest of the wording.
When the user wants to write part of each message themselves, put that part in double
square brackets, like [[where you met]]: the run leaves it as written, and each message waits in
their approval queue, even on a step that would otherwise send on its own; approving it
is refused until they fill it in there. Every other part of the queued message is
complete as it stands, one the user could approve without changing a word (the queue
rejects a leftover {slot}).
The templates are saved with the step and come back on get_campaign_flow; omit
message_templates to leave a step's templates as they are, or pass [] to remove them.
To A/B test a step's message, add a second version to the list: prospects
reaching the step alternate between the two, and get_message_experiments reports which gets
more interested replies. At current volumes a test takes weeks and only separates meaningfully
different messages (a different opener or ask, not a reworded phrase), so propose versions
that differ that much. Changing either version's text restarts the test. To end a test,
pass just the version you keep.
A connection_request node's message_templates holds its note the same way (at most 300
characters, filled per prospect), where "" is a version sent with no note: ["", note] A/B
tests a note against none, and get_message_experiments reports which gets more requests
accepted.
Anything else the run should know for that step (tone, who to mention, what to skip,
whether it may rephrase the template, what to write when a slot can't be filled) goes in
the step's on_enter trigger prompt via update_trigger — keep
the message itself in message_templates, not in that prompt or in workspace.inputs, so
the user sees and edits it on the step. When you first set a template on a step whose
prompt already carries its message, rewrite that prompt down to the extra instructions.
(workspace.inputs stays the home for a flowless
campaign's templates and for per-agent data no single step owns — criteria, audience,
exclude lists.)
Authoring (or replacing) a sequence on an agent that ALREADY holds untouched prospects enrolls that whole backlog into it — so before defining a sequence on an agent with an existing roster (a monitoring or engager-capture agent especially), confirm with the user that the accumulated prospects should all enter the campaign.
Re-authoring replaces the prior sequence (allowed on a live campaign); a prospect still
resting on a dropped node returns under off_sequence_nodes (with a note on re-homing)
rather than being lost. The node-owned on_enter triggers reconcile in the same call:
every node kept in the new DAG (same id, any kind) keeps its on_enter row and the
prompt/code on it, a new enactable node gets a fresh row auto-minted, and only a node
dropped from the DAG entirely has its on_enter row deleted. A new resolve /
connection_request / follow / instantly node is minted with a default deterministic enactment
code (it enacts clean with no agent run — edit it for a non-standard step; a connection
request's note goes in its message_templates, not the code); every other
kind mints code-less. A reused id keeps its existing row — so after changing what that id's node
does, review its on_enter content; an unmodified minted default is regenerated for the new
kind automatically, but an edited one is left as you wrote it.
Inputs: agent_id (required), sequence (required), on_behalf_of.
delete_agentDelete agentADestructiveInspect
Inputs: agent_id (required), on_behalf_of.
delete_message_tagDelete message tagADestructiveInspect
Inputs: tag_id (required), on_behalf_of.
delete_message_tag_groupDelete message tag groupADestructiveInspect
Inputs: group_id (required), on_behalf_of.
delete_segment_groupDelete segment groupADestructiveInspect
Inputs: group_id (required), on_behalf_of.
delete_segment_tagDelete segment tagADestructiveInspect
Inputs: tag_id (required), on_behalf_of.
detach_from_outreachDetach from outreachAInspect
id is the attachment's id as shown on that queued message in the queue status read
(manage_email_outreach_queue or manage_linkedin_invite_queue, action='status').
The removed file's name and the files still on that message, shaped
{'detached': True, 'filename', 'attachments': [{'id', 'filename', 'mime_type', 'size'}]}.
Inputs: id (required), on_behalf_of.
draft_emailDraft emailAInspect
Use search_emails or search_linkedin_message_history to find email addresses for prior correspondents. For new contacts whose email you don't have, use find_email. Dict with success, draft_id, provider, and draft details (to, cc, bcc, subject, body)
Inputs: to (required), body (required), subject (required), cc, +3 more.
draft_replyDraft replyAInspect
Inputs: body (required), email_id (required), cc, bcc, +2 more.
edit_monitor_membersEdit monitor membersAInspect
add and remove are LinkedIn profile URLs or vanity slugs for people, and
full linkedin.com/company/... URLs for company pages (a bare company name or
slug is read as a person's slug). A swap is one call
(add=[new], remove=[old]). Both forms of one profile (full URL and bare
slug) collapse to a single watched member, so re-adding someone already watched
is a no-op. The change takes effect on the monitor's next scheduled run.
Use this instead of re-running set_up_linkedin_monitoring whenever the user just
wants to tweak who's watched ("also watch Bob", "drop Alice"): setup would force
re-passing every existing member, and since each watched profile is billed per
run, one dropped URL on the re-echo silently stops watching that person. To
change schedule, voice, or the action flags, use set_up_linkedin_monitoring
(members are preserved there when omitted); adding the very first members or
resetting the whole list is setup too.
Dict with status ('active'), name, members_count, added, removed,
unresolved, and message.
Inputs: name (required), agent_id (required), add, remove, +1 more.
enrich_linkedin_profilesEnrich LinkedIn profilesARead-onlyInspect
This is the tool for questions about a list of people: filtering a list by
connection count or seniority, labelling who works where, or filling in
headlines before drafting. experience carries every role with its dates,
so it also answers career-history questions — how long someone has been in
seat, where they worked before, whether they were promoted internally, who
is an alum of a given company — without a separate lookup. It does not touch
the user's LinkedIn account, so it neither consumes their daily
profile-lookup budget nor carries any account-safety risk — prefer it over
per-person lookups whenever you have more than a couple of people to enrich.
Costs 0.1 Sliq credits per profile returned; misses are free. A profile this user enriched in the last 24h is served from cache, so re-calling does not look up or charge again.
Enriching a profile also links it to its primary employer's canonical company, which may need a one-time web-domain lookup: 0.05 credits the first time a given company is looked up (cached after, so it never charges twice for the same company). A batch spanning many unfamiliar employers costs a little beyond the per-profile total; fold that into any estimate you give the user.
How many to run is a spend question: run a list of up to 500 straight away. Past 500, tell the user how many profiles it is and what that costs — the count times 0.1 credits — and wait for a go-ahead before running it; a batch that size also takes several minutes, so say so in the same breath. In a background run there is nobody to ask, so run it and report the spend in your summary.
How to call it is a separate question, and the answer is almost always
run_code. A direct return is truncated at 50KB, and one senior profile's
career history can be a third of that on its own — so a direct call on a
dozen executives shows you two of them, after charging for all twelve, since
credits are spent inside the tool before anything is truncated. Only a
handful of profiles fit. From run_code nothing is truncated: the rows stay
in the sandbox and you print only the filter, count, or summary you need.
Call it directly only for a few people whose full profiles you intend to
read.
What it cannot tell you: whether the user is already connected to someone,
their network distance, or shared connections. Those describe the user's own
relationship to the profile and only a LinkedIn-account lookup can answer
them — use queue_linkedin_outreach(action_type='resolve') when the decision
genuinely depends on connection status.
One entry per input, in input order — either a profile dict, or an
{'error': ...} entry for a profile that could not be resolved —
followed by any warnings. The error says which case it is: no profile
exists for the identifier, or the lookup did not finish and the
identifier should be retried.
experience and education cover the person's whole history and are
long for senior people — a 25-year career can run 20+ roles. They arrive
in LinkedIn's display order, which is NOT sorted by date: roles at the
same employer sit next to each other, so the first entry is not reliably
the current one. Sort on start_date.get('year') when you need
chronology. Consecutive entries at one employer are usually one tenure
with internal promotions, but check the dates before saying so — a gap
between them means they left and came back, which is a different story
to tell. Dates are {'year': 2014, 'month': 'Feb', 'text': 'Feb 2014'};
month is missing when LinkedIn shows only a year, and a date can be
empty entirely, so reach for .get() rather than indexing. An
end_date of {'text': 'Present'} means the role is current, and
someone can hold several at once.
Inputs: linkedin_urls (required), on_behalf_of.
escalate_to_teamEscalate to teamAInspect
Sliq stamps the affected user, the surface, and the agent automatically, so write
the summary for a teammate with no other context: what the user wanted, what went
wrong, and what you already tried.
Dict confirming the flag was raised, with a note on what to tell the user next.
Inputs: summary (required), agent_id, on_behalf_of.
estimate_message_tag_classify_scopeEstimate message tag classify scopeARead-onlyInspect
Inputs: channel, senders, agent_ids, positions, +2 more.
estimate_segment_classify_scopeEstimate segment classify scopeARead-onlyInspect
Inputs: senders, agent_ids, on_behalf_of.
exa_find_peopleExa find peopleAInspect
Use AFTER companies have been identified (company-find step in the same agent, user named a target, CRM lookup, etc.) to surface specific decision-makers. Results are always LinkedIn URLs.
Before composing a search, call get_skill_guide('exa_find_people') —
it owns the query vs criteria split that decides whether
results come back useful or noisy (exclusions in query backfire;
they belong in criteria).
Every person a search returns is checked against its criteria, one
at a time and whichever provider served it, and results holds only
the matches, most senior first; non-matches come back in rejected as
{name, reason} with no URL. Put exclusions in criteria.
Pass company_domain and company_linkedin_url whenever you have them. A
search carrying either goes first to people databases that filter on that
identifier (Apollo filters on a domain only), so they cannot return
someone at a same-named company.
Without one, or when those find nobody, the search goes to a
name-matched provider, and the check is what keeps a same-named
company's staff out, reading each person's current and past roles
against criteria.
Every provider reads a profile snapshot refreshed periodically, not live reality, so someone who changed jobs since the last crawl can still pass.
Accepts a list of ExaPeopleSearch — pass one for a single-company
lookup, or N for a batch, fanned out in parallel (max 5 concurrent, since
the name-matched provider has shown 5xx instability under bursty load).
Each search routes on its own, so one batch can be served by different
providers. Each call retries once on 5xx inside the HTTP client.
An identifier-pinned search matching more people than can be returned at
once comes back in its own slot as {error, query} asking you to narrow
by titles (and locations, at a multinational); the rest of the batch
is unaffected. Re-send that one search narrowed.
Costs Sliq credits by what the provider that answered bills: 0.04 per
person an identifier-pinned search returns, or 0.3 per name-matched
search plus 0.04 per candidate past 10, so a larger num_results costs
more. The user's own connected Airscale, Exa or Apollo key is tried
before Sliq's providers, and a search it answers is free. When the
balance runs short mid-batch, the data is still returned and the charge
is capped at what remains.
List of per-search result dicts in input order. Each is either
{results: [...], rejected: [...]} on success, or
{error: '...', query: '...'} on failure (after the in-client
retry). results entries are {name, url, summary, verdict, verdict_reason, seniority_level}, most senior first, with url
always a LinkedIn profile URL, summary holding name, current_title,
current_company, location and tenure, verdict 'qualified', or
'unclear' when the profile says too little to judge, and
seniority_level one of c_level, vp, director, senior_manager,
manager, ic, unknown (both null when the check couldn't run).
rejected entries are {name, reason} only.
When the user is out of Sliq credits and has no key of their own, the
batch is refused before running: it raises InsufficientCreditsError
rather than returning per-search slots. With a key of their own, the
batch runs on it alone, and a search none of their keys can take
comes back as an {error, query} slot saying they're out of credits.
Inputs: searches (required), agent_id, list_name, num_results, +1 more.
exa_search_newsExa search newsAInspect
A find-companies News-mode discovery source, used alongside
search_news_events and tavily_search — the News-mode skill owns
the routing (which signals fan out here). Each query retrieves the
top recent news articles matching it, and each article comes back as
pre-extracted company rows (name, domain, one-line event), so unlike
tavily_search there is no article-extraction step. Covers narrative
signals PredictLeads' typed categories miss (AI initiatives,
role-scoped exec changes, plant incidents) and fills its zero-result
gaps on typed events.
Compose queries in positive form describing the event sought, e.g. "manufacturing companies announcing new plant openings in the United States" — one query per signal, all in one call (the tool fans them out in parallel; one tool call is one LLM round). Don't embed exclusions — vector retrieval has no concept of "not", so naming an excluded term biases results towards it.
Each query retrieves the 10 most relevant recent articles. Pass
lookback_days to bound how far back to look (default 30); articles
published before that window are excluded server-side. A news scan
passes the window from its trigger prompt.
Costs 0.5 Sliq credits per query that returns without error, so a batch of 4 queries costs 2 credits; a failed query is free. Free on BYO Exa (a connected Exa key). Soft-fails on insufficient credits: the charge is capped at the remaining balance.
Each company is then checked against criteria, one company at a time,
reading its name and its article's headline and event, and only the
matches come back in companies; the rest come back in rejected as
{name, reason}. It adds a few seconds per ten companies.
When this runs in an agent, every company with a domain is saved to the
Output tab as a company row (deduped by domain) with its verdict,
carrying its article in data.signals — except a company whose domain
is the article's own host, which is often the outlet's domain misread as
the company's. The rejected ones are hidden from the list's default
view.
Dict with companies, the matching companies; rejected, the
companies the check turned down, as {name, reason}; count, every
company found, rejected ones included; and created_identifiers, the
domains of matching companies this call newly added to the list.
Each company row is {company_name, domain, event, headquarters, source_url, title, published_at, query, verdict, verdict_reason} —
verdict is 'qualified', 'unclear' when the row says too little to
judge, or null when the check couldn't run; domain is
lowercased-bare or None when the article didn't reveal one (resolve
those downstream before recording), event is a one-line
description of what happened, source_url/title/published_at
identify the article, and query names the originating query. Rows
are deduped by domain across queries (first article wins).
If every query failed with an upstream error and nothing landed,
also exa_available: False plus a human-readable error — a
source outage, NOT a genuinely empty feed. Partial failure (some
queries errored, others returned) stays silent and reports only
the companies.
On insufficient Sliq credits the call is refused before running —
it raises InsufficientCreditsError instead.
Inputs: queries (required), criteria (required), agent_id, list_name, +2 more.
extend_find_searchExtend find searchAInspect
Add more results to a named list on an agent.
Reuses the list's original criteria — does NOT support refinement. If the user wants to narrow the criteria, tell them to start a new list (call start_find_search with a fresh list_name).
Refuses when: no webset on file for the list, a run is already in
progress, the per-list cap is already reached, or (sliq mode) the balance
can't afford even one more result. When the balance covers fewer than
requested, the count is clamped and the result carries a clamp_notice —
surface it to the user.
Inputs: agent_id (required), list_name (required), on_behalf_of, additional_count.
fetch_linkedin_messages_with_personFetch LinkedIn messages with personARead-onlyInspect
Query the local store with search_linkedin_message_history first. Reach for
this tool only when that misses, when you need messages older than the
roughly two-week local backfill window, or when you must be certain whether a
conversation exists — this reads LinkedIn's own synced history, so an empty
result here means no such conversation, not merely "outside the local
window". Fetched messages are written back to the local store, so a later
search_linkedin_message_history sees them too — unless whoever's history
this is keeps only prospects' data and this person isn't one of their
prospects; then they're returned but not stored.
This is not a name-lookup tool: pass the person's LinkedIn provider_id,
sourced from a prospect row, search_linkedin_connections, or a prior local
message — never guessed from a name.
On success, a dict with success=True, chat_id (the chat holding the
newest message; pass to queue_linkedin_outreach with
action_type='message' to reply), count,
truncated (True when older messages exist beyond the fetch cap),
messages — newest first, each with direction
('sent'/'received', or 'unknown' when LinkedIn omits the sender flag),
message_datetime (ISO), content (truncated to 1000 chars; empty when
the message is only files) and attachments (its files, as in
search_linkedin_message_history; to read one, find the message there
and pass its id to read_linkedin_attachment) — and
connection_request_note: the note on your accepted connection request,
which LinkedIn replays into the chat when they accept, as
message_datetime + content (null when there's none). It's the invite,
not a message you sent them, so it's kept out of messages and count. On
failure, success=False and error; an error that mentions
reconnecting means the user must reconnect LinkedIn in Settings.
Inputs: provider_id (required), on_behalf_of.
fetch_post_engagersFetch post engagersAInspect
Visibility follows the user's LinkedIn account: any post it can view works, and private or deleted posts return a plain error. The list is capped at 500 reactions + 500 comments per post; on a bigger post the return flags the cut-off. A fetch costs 0.04 credits per reaction, comment or reply it pulls, people already stored included, so re-fetching a post bills everyone on it again; data fetched within the last 6 hours is served from the database free. Loading a post Sliq hasn't tracked yet counts one against the user's daily LinkedIn post-read budget. In a conversation, tell the user the cost and the wait before calling this: most posts cost under 5 credits, one fetch's reactions and comments cost at most 40 plus 0.04 per reply, and a post with hundreds of engagers takes around half a minute.
When this runs in an agent, every person who engaged is saved as an
agent_search_results person row, rendered in the agent's Output tab and
queryable via query_search_results: data.person carries the name and
headline, linkedin_provider_id and linkedin_slug the LinkedIn handles, and
data.columns the engagement (Engagement, Comment, Post, Post URL). Company
pages that reacted or commented aren't people, and the user isn't a lead, so
neither is saved; both stay in the engagement rows. Re-running updates
existing rows rather than duplicating them. Pass list_name to name their list; absent, they land in the
'default' list.
Each person new to the list is checked against criteria, one at a
time, reading their headline and engagement, and saved with the
verdict; the rejected ones are hidden from the list's default view and
from query_search_results. The check adds about 20 seconds per
hundred people. To keep everyone who engaged, write broad criteria,
such as "Anyone who engaged with this post". A person already on the
list keeps their verdict, so to screen a post's engagers against new
criteria, pass a new list_name.
After this returns, read the saved people with the query_search_results
call in next_step, which is scoped to this post: one list can hold
several posts' engagers. Judge anyone the check kept as unclear or left
without a verdict yourself, then queue outreach for the people who fit
with one batched queue_linkedin_outreach call passing each row's
linkedin_provider_id as provider_id and its linkedin_slug as
linkedin_slug. Send-time resolution skips anyone already connected, so they
don't need pre-filtering here. Every engager, the rejected ones included,
also stays queryable with query_linkedin_post_engagements using
post_id = <post_analytics_id>.
On success, a dict
{'success': True, 'post_analytics_id': int, 'author_name': str,
'is_own_post': bool, 'post_text_snippet': str, 'from_cache': bool,
'reactions_stored': int, 'comments_stored': int,
'unique_engagers': int, 'truncated': bool, 'credits_charged': number,
'budget': {'used_today': int, 'limit': int},
'engagers_preview': [first 10 engagers, unchecked], 'next_step': str,
'list'?: {'list_name', 'created', 'updated', 'total', 'rejected'}}.
comments_stored counts replies too. unique_engagers counts company
pages too; list.total counts only the
people saved, rejected ones included, so it can be lower. list.created
and list.updated count the people the list shows, and list.rejected
the ones the check turned down.
budget is today's LinkedIn post-read budget.
On failure, {'success': False, 'error': str} when the post isn't visible to the
user's account, the daily LinkedIn post-read budget is spent, or
LinkedIn actions are paused after a rate limit. Raises when the user's
credits can't cover a fetch.
Inputs: criteria (required), post_url (required), agent_id, list_name, +1 more.
find_companies_by_tech_stackFind companies by tech stackAInspect
Use ONLY when the user explicitly wants to discover companies BY THEIR
INSTALLED TECHNOLOGY — e.g. "find DTC brands using Shopify and Klaviyo",
"who runs Snowflake AND Looker", "US e-commerce companies using HubSpot".
For hiring-signal discovery (companies posting jobs that mention a tech),
use theirstack_search instead — that surfaces investment intent,
whereas this surfaces installed base.
That installed base is what TheirStack DETECTS from job-posting text, not from crawling storefronts, so it only sees companies that hire and name the tool in their JDs — treat a company's absence as "not detected here," not "not using it" (a storefront-sniffing method like BuiltWith/Wappalyzer would surface more).
For ecommerce-platform tools, results may include the platform's SERVICE
PROVIDERS (agencies, ISVs) alongside actual merchants — job mentions don't
distinguish "we run on Shopify" from "we sell to Shopify merchants," so
name the kind of company you want in criteria and the check filters
them out. Recruiting agencies are always excluded (company_type =
direct_employer), matching theirstack_search.
Costs 1.5 Sliq credits per company returned; a failed search is free.
In chat, STATE THE COUNT AND THE COST in the same reply as the
results — every time, without stopping to ask first. Use the user's
number when they gave one, otherwise the default: "pulled 25
companies — 37.5 credits; say if you want more, max 100." Price the
companies actually returned, which count gives: fewer than limit
costs less. Because this endpoint pages, prefer one page at the
user's number over silently walking offset past it — every extra
page is more credits.
Each company returned is then checked against criteria, one company
at a time, reading its name, firmographics and technologies, and only
the matches come back in companies; the rest come back in rejected
as {name, reason}. It adds a few seconds per ten companies.
When this runs in an agent, every company with a domain is saved to the Output tab as a company row (deduped by domain) with its verdict, carrying its firmographics and matched technologies; the rejected ones are hidden from the list's default view.
The tool resolves each technology name to a TheirStack catalog slug
(calling /v0/catalog/technologies per name; popularity tiebreak; exact
name match wins), then queries /v1/companies/search with
company_technology_slug_and so EVERY supplied technology must be
present on the returned company.
{
"companies": [
{
"id": str, "name": str, "domain": str | None,
"industry": str | None, "employee_count": int | None,
"country_code": str | None, "linkedin_url": str | None,
"technologies_found": [
{"slug": str, "name": str, "confidence": str,
"jobs": int, "last_date_found": str},
...
],
"verdict": str | None, # 'qualified', 'unclear', or None when the check couldn't run
"verdict_reason": str,
},
...
],
"rejected": [{"name": str, "reason": str}, ...], # companies the check turned down
"count": int, # companies in this page, rejected ones included; the number billed
"total_matches": int, # universe size for this query (or None)
"created_identifiers": [str], # domains of matching companies this call newly added to the list
}
On upstream failure (timeout / 5xx / connection error), returns
{"companies": [], "count": 0, "theirstack_available": False}
so the agent can read the flag and degrade gracefully.
Inputs: criteria (required), technologies (required), limit, offset, +11 more.
find_emailFind emailARead-onlyInspect
Costs 1 Sliq credit per verified email; a BYO-Apollo key makes the Apollo
leg free. The default chain runs MoltSets first and fills its misses with
Apollo, so a contact that comes back {'error': ...} has been tried by both
providers and is final — report those gaps and move on rather than
re-running them.
When only one provider ran — a BYO-Apollo user's default, or an explicit 'apollo' pin — the other is still untried, and the result says so. That paid lookup is the user's call: in a background run with no one to answer, report how many were missed and stop; in an interactive chat, tell them the count and offer a MoltSets backfill (1 credit/hit), re-calling with provider='moltsets' on just those contacts only after they say yes.
A contact this user already resolved in the last 24h is served from a cache,
so re-calling does not look up or charge again.
One entry per input contact, in input order — either a hit dict
(email, email_status, name, title, linkedin_url) or an
{'error': ...} miss — followed by any warnings. A hit's linkedin_url is the
person's LinkedIn profile URL; a recipient or person takes it as linkedin_slug.
Each verified email is also written for you onto the person's canonical
profile if it had none, which is the address every campaign tracking that
person sends to and the one the Email column on the Output tab shows
wherever they're on a list — you do not match or persist emails back onto
rows yourself. It adds no one to an agent's list: when a person found here
who isn't on it yet belongs there, record them with record_search_results.
Inputs: contacts (required), provider, on_behalf_of.
find_linkedin_urlFind LinkedIn URLARead-onlyInspect
For email outreach where the agent might later add LinkedIn touches, prefer
queue_email_outreach(enrich=True) over calling this tool separately —
enrich folds the lookup into the queue boundary so the resulting tracking
row carries both identifiers.
Costs 1 Sliq credit per verified hit. Free when the user has connected
their own Apollo key.
Dict with success, linkedin_url, and person fields when verified;
{success: False, error: ...} when Apollo returns no match; a 429
throttle adds rate_limited: True (transient — retry later, not a
permanent miss).
Inputs: email (required), name, on_behalf_of.
find_phone_numberFind phone numberARead-onlyInspect
Pass a list of LinkedIn profile URLs — one entry for a single person, all of
them at once for a batch (they are looked up in parallel, costing one tool
call against the loop guard, not N). Returns one result per URL in the same
order. Airscale brokers providers like RocketReach. Persists nothing — use
the returned numbers however the agent needs them (e.g. tell the user, or
stash them on prospects via update_prospect).
A LinkedIn profile URL is the only accepted input — this tool cannot look a
person up by email or name. When a record (a HubSpot/CRM contact, a prospect
row, a spreadsheet line) has no LinkedIn URL but does have the person's
email, first call find_linkedin_url(email=...) to resolve one, then pass
that URL here. Apply this resolve-then-lookup step to every record, not just
the first; skip the phone lookup for any person you cannot get a URL for.
Costs 8 Sliq credits per number found, when run on Sliq's shared Airscale
account. Misses (no number on file) and upstream failures are free. A
worst-case check runs up front on the Sliq path: the whole batch is refused
unless the balance covers 8 credits per URL (any URL could be a hit). So
no API call is ever spent on a lookup that can't be billed, and a user low
on credits is told to top up or connect their own key. If the user has
connected their own Airscale key (BYO, via the integrations page), lookups
run against that account instead — no Sliq credit gate and no Sliq credit
charge.
A list of result dicts in input order.
Per hit: {status: 'success', found: True, phone_number, phone_numbers, provider, linkedin_url, credits_charged: 8} (0 on the BYO path) —
phone_number is the first number, phone_numbers the full list.
Per miss: {..., found: False, phone_number: None, phone_numbers: [], credits_charged: 0}. A malformed (non-LinkedIn) URL or
an upstream failure on one URL becomes a per-item {found: False, error, credits_charged: 0} in that slot — it never aborts the rest of the batch.
If the up-front worst-case credit check fails, it raises
InsufficientCreditsError (no lookups are attempted).
Inputs: linkedin_urls (required), on_behalf_of.
find_warm_intro_pathsFind warm intro pathsARead-onlyInspect
Pass targets as a list of LinkedIn identifiers — profile URLs, username
slugs, or provider_ids — for the people the user wants to reach. Runs in the
background, pacing 1-5 minutes between targets to stay within LinkedIn's
safety limits; results land as one row per target under list_name in the
agent's entity store — read them with query_search_results(agent_id, where_clause="list_name = '<list_name>'"). Each row carries the mutual
connections — the shared 1st-degree connections who can introduce the user.
For each target the search returns at most 10 mutual connections. A target that comes back with all 10 may have more the search didn't surface — treat that target's list as a sample rather than the complete set, and say so when you report it.
list_name is the bucket the target rows land in. Pass a short descriptive
slug naming this set of targets (e.g. 'acme-intros', 'series-a-leads');
distinct slugs let one agent hold several independent warm-intro searches,
and reusing a slug accumulates into one list.
Each target costs 1 profile lookup + 1 LinkedIn search against the daily budgets (~50-60 lookups for an established account, a quarter of that while a new one ramps up; the LinkedIn search budget is shared with all people searches), plus 1 credit only when a mutual connection is found for that target. An already-connected target with shared connections still costs that credit — the warmer path is still worth surfacing. Checking a target also links it to its primary employer's company, which may need a one-time web-domain lookup: 0.05 credits the first time a given company is looked up (cached after, so it never charges twice for the same company). Targets beyond today's budget are deferred — re-run tomorrow to continue. Confirm with the user before passing a large list. Only one warm-intro search can run at a time per account. A status dict in one of three forms. {'success': True, 'status': 'running', ...} on kickoff {'success': True, 'status': 'queued', ...} when another warm-intro search already holds the account's single slot — this one is queued behind it and starts automatically when the active one finishes {'success': False, 'error': str} on validation / budget failure
Inputs: targets (required), list_name (required), on_behalf_of.
generate_monitored_post_commentGenerate monitored post commentADestructiveInspect
Prefer this over queue_linkedin_post_engagement (with your own text) when acting
on a post from the agent's monitored-posts feed. Pass a row id from
query_monitored_posts. Only posts with no queued comment are eligible
('filtered', 'skipped', 'skipped_no_credit', 'captured', 'surfaced');
already-commented, queued, or user-rejected posts are refused.
A queued-status dict in one of three forms.
{'queued': True, 'awaiting_approval': bool, 'status': 'queued', 'draft_comment': str, 'agent_id': int, 'message': str}
on success (1 credit charged); agent_id is the agent monitoring the post
{'queued': False, 'deduped': True, 'message': str} if a comment is already queued
{'queued': False, 'error': 'not_found'|'not_eligible'|'out_of_credits'|'no_draft'} otherwise,
or with an error sentence when the draft ran too long for LinkedIn
Inputs: monitored_post_id (required), on_behalf_of.
get_agent_detailsGet agent detailsARead-onlyInspect
list_agents returns only one-line summaries — call this to get the full
picture for a specific agent. Run-level history is
returned here as recent_runs; each entry carries its run id, which you
pass to get_run_transcript for that run's full tool-by-tool transcript.
Agent-specific operational learnings are returned as a top-level learnings
list (managed via append_learning).
Dict with agent fields, workspace, learnings, recent_runs, recent_runs_truncated
(True when older runs remain past this page of recent_runs), and tracking_summary.
When the agent has prospects, tracking_summary.funnel carries the derived outreach
rollup the agent workspace shows — per-channel funnels, connection
acceptance_rate, reply_rate, and the people-level lead_funnel — so read rates
and lead buckets from there rather than deriving them off the raw stage counts.
lead_funnel.reached holds the numbers on the funnel's rungs (per rung, everyone who got
at least that far), except reached.accepted: the people who accepted a connection request
this agent sent them, out of lead_funnel.invited, the people it sent one, as
lead_funnel.accept_rate. lead_funnel.rates {accepted of invited; replied, interested,
meeting_booked of contacted} and lead_funnel.step_rates {interested of replied,
meeting_booked of interested} hold whole percents, null where the base is 0.
lead_funnel.targets holds the LinkedIn target for each rates key, as a whole percent
of the same base, each value null where it doesn't apply: accepted when no connection
request went out, the others when anyone in the funnel is on email or nobody is on
LinkedIn. The lead_funnel's other stage keys count each person once, at their furthest
stage, so together they add up to lead_funnel.prospects.
lead_funnel.removed counts the people removed from the campaign, who stay at the stage
they reached, so it overlaps those keys; list_people with the agent_id and a "removed"
filter of 'removed' lists them.
mcp_changes lists changes made outside Sliq by the user's or a teammate's MCP clients
(such as Claude) calling Sliq tools directly, newest first, one entry per burst of calls
to one tool: id, tool_name, client_label (with the teammate's email when a teammate
made it), count, first_at, last_at, and latest_arguments (the burst's latest
call's arguments). Pass an entry's id as change_id to
get_mcp_change_calls to read every call in the burst.
Inputs: agent_id (required), on_behalf_of, recent_runs_limit, recent_runs_offset.
get_campaign_flowGet campaign flowARead-onlyInspect
sequence is the authored DAG (null for a flowless campaign): sequence.start
is the origin node and sequence.nodes the body nodes, each with its id, label, and
kind (start / connection_request / send / action / manual / gift / instantly / automation /
decision / terminal) plus that kind's typed routing — a connection_request's accepted /
already_connected / timeout / message_templates (its note), a send's channel / action / replied / follow_up /
message_templates (two while it A/B tests a second version), a silent action's channel / action / then, a manual node's
action_description / then, a gift node's product_url / max_price_usd / gift_note /
then, an instantly node's campaign_id / replied / no_reply (the Instantly campaign
it hands prospects to, where a reply moves them, and where a prospect who hasn't replied moves
once the sequence has ended and no_reply's delay has passed since Instantly's last email to
them; each prospect's handoff is the instantly_handoffs field on their query_prospects
row), an automation node's then (its side-effect is authored on
its on_enter trigger), a decision's rule / arms, a terminal (a sink). Timed edges carry {value, unit} (unit
w/d/h/m). A
connection_request node is always LinkedIn (its channel/action are implicit in its kind);
the resolve stage is the action == 'resolve' silent-action node — it looks each profile
up, then the campaign flows onward. Walk sequence to see which step feeds which and what
gates each hop; join it with nodes by node id for per-node occupancy.
nodes is the per-node occupancy map, keyed by node id: by_status, current, blocked,
pending, withdrawn, removed, and off_sequence. It covers every sequence node plus any
foreign id the queue throughput or the cursor carries. The start node's current counts the
prospects not yet routed onto any node. withdrawn counts the prospects resting on a
connection_request node whose request Sliq auto-withdrew unaccepted: they no longer wait for
an accept there, and the node's timeout, if it has one, still moves them on. removed
counts the removed prospects resting on that node (on the start node, the removed ones not
yet routed), auto-withdrawn ones included, so no prospect counts in both. Both still rest
there, and are left out of current, blocked, and pending. off_sequence is True for a node id prospects rest on that the
current sequence no longer defines — a position an earlier flow edit stranded — surfaced so
those prospects stay visible (such an id is absent from sequence, so read its counts here).
pause is the per-scope pause breakdown (null for a non-outreach agent): campaign.paused
holds every action node; linkedin.reason (needs_reconnect / auto / manual, with until
on an auto rate-limit pause) holds every LinkedIn action node while email stays live;
connection_request.until holds only connection_request nodes; and steps.paused lists the
node ids whose own step is paused — nothing new is composed there until it resumes, though
sends already queued still go out. Read it to answer "why is
step 3 not firing" per node — a paused scope explains a node with pending occupants but no
throughput.
Dict with sequence (the authored DAG as stored, or null for a flowless campaign),
nodes {node_id: {by_status, current, blocked, pending, withdrawn, removed, off_sequence}}, and pause
(per-scope pause breakdown; see above). blocked is how many resting prospects have a
blocked enactment (see report_enactment_blocked; clear one for retry with
retry_blocked_enactment); pending how many rest on an action/manual/decision node
whose step hasn't run for them yet — for a manual node, awaiting the human who performs
it — no intervention needed, unlike blocked. Null sequence and empty nodes for a
flowless campaign with no queue activity.
Inputs: agent_id (required), on_behalf_of.
get_chat_messagesGet chat messagesARead-onlyInspect
Inputs: session_id (required), limit, offset, on_behalf_of.
get_credit_usageGet credit usageARead-onlyInspect
Current Sliq credit balance, 30-day spend by agent, the card saved for gift purchases, and — for a workspace member — the shared team block.
Returns {'balance': float, 'usage_by_agent': [{'agent_id', 'title',
'credits_used', 'is_agent'}, ...], 'gift_card': str, 'team': None |
{'workspace_name', 'your_role', 'seats_used', 'seats_active', 'seat_limit',
'per_member_spend': [{'user_email', 'credits_used_30d', 'is_you'}, ...]}}.
gift_card is the label of the card the user saved under Settings → Billing
for gift purchases (e.g. 'Visa •••• 4242'), or '' when no card is saved.
balance/usage_by_agent are the same rollup the Billing page renders;
spend not tied to an agent groups by activity label (e.g. 'Standalone chat', 'Data
extraction'). team is None for solo users and otherwise mirrors the
Billing team card: seats_used counts invited+active seats (spoken for),
seats_active only active ones. per_member_spend excludes removed members
and unattributed charges, so it does not sum to pool spend — it is spend over
the last 30 days.
Inputs: on_behalf_of.
get_email_attachmentGet email attachmentARead-onlyInspect
Provide email_data_id + attachment_id (from search_emails results). Dict with filename, mimeType, size, and url (CloudFront URL for the file)
Inputs: on_behalf_of, attachment_id, email_data_id.
get_emailsGet emailsARead-onlyInspect
Inputs: ids (required), on_behalf_of.
get_integration_statusGet integration statusARead-onlyInspect
Inputs: on_behalf_of.
get_linkedin_monitorsGet LinkedIn monitorsARead-onlyInspect
Default (no monitor_id): one summary per monitor with members_count (the
number of watched LinkedIn profile and company page URLs — list mode only, 0 for network/topic)
and members_sample (the first 5 URLs), not the full roster, since a list
monitor can watch thousands. To read a monitor's full roster, pass its id
(returned on each summary) as monitor_id; that returns the URL list paged by
offset/limit with a truncated flag — re-call with offset += limit while
truncated. The URLs are in the exact shape set_up_linkedin_monitoring takes
back. run_weekdays is weekday ints Mon=0 … Sun=6 in the user's timezone.
last_run_at is null before the monitor's first run.
By default, a dict
{'monitors': [{'id', 'name', 'mode', 'is_active', 'members_count',
'members_sample', 'keywords', 'writing_instructions', 'post_filter',
'draft_comments', 'fetch_engagers', 'capture_authors', 'engager_icp',
'engager_instructions', 'run_weekdays', 'recency_days', 'last_run_at'}, ...]} — an empty list if the
agent has no monitors. With monitor_id: {'monitor_id', 'name',
'members_count', 'members' (this page's URL list), 'offset', 'limit',
'truncated'}. post_filter is the user's own description of which posts
the monitor keeps (blank keeps every post). engager_icp
filters who capture collects (blank means everyone) and engager_instructions
is what to do with them (blank means take no action); both are blank when
fetch_engagers and capture_authors are both off, and both need re-passing when
re-arming capture.
Inputs: agent_id (required), limit, offset, monitor_id, +1 more.
get_linkedin_postsGet LinkedIn postsARead-onlyInspect
RECENCY WINDOW: only posts from the last posted_within_days days (default 7)
are returned — this tool answers "what has this person posted lately", not
"show me their post history". A profile with nothing in that window comes back
with profiles[<key>]['status'] == 'no_recent_posts' and no posts for that
profile — that is normal and does not mean the lookup failed. In that case,
personalize off the prospect's role, company, or headline instead of forcing a
stale post reference; do not widen posted_within_days just to find something
to quote unless the user asked for older posts specifically.
Only the profile's own original posts count as "recent activity" — reshares and quote-posts are excluded.
ALWAYS BATCH: pass every profile URL in ONE call — batching is both cheaper and
faster than one call per profile. Up to 1000 profiles per call; split a larger
list across calls. Each returned post carries a profile_input field identifying
which profile it came from (the matched input identifier).
COST: 0.02 credits per unique scrapeable profile searched, PLUS 0.5 credits for
each profile that actually has a post in the window. If the user has fewer
credits than profiles, only the affordable first profiles are looked up and the
rest are reported in skipped_profiles_due_to_credits.
LARGE-BATCH COST GATE: because each profile can cost up to 0.52 credits, a call
that would search more than 100 profiles is refused with a ModelRetry that
states the exact credit cost, UNLESS large_batch_approved=True is passed. Set
large_batch_approved=True ONLY after the user has seen the credit
cost and agreed to it — in interactive chat, that means you told them the number
and they said yes; in stored trigger code, ONLY if the user explicitly approved
this recurring spend when the trigger was set up. Do not set it reflexively to
silence the retry.
A dict with the following keys.
posts: list of {url, text, author, posted_at, days_ago, profile_input, reactions, comments} — newest-first per profile.
reactions/commentsare engagement COUNTS, not the people who engaged — usefetch_post_engagersfor the actual list of people.total: number of posts returned.
profiles: dict keyed by the normalized profile identifier (the same value as each of that profile's post's
profile_input), each{'status': 'ok'|'no_recent_posts'|'not_found', 'posts': int}— covers every attempted or cached profile.not_foundmeans the actor could not resolve the target (renamed/private/deleted);no_recent_postsmeans it resolved but nothing fell in the window. (Distinct from the top-levelunresolvable_profileslist below, which is inputs rejected at URL classification and never sent to the actor.)posted_within_days, profiles_lookup_count, credits_charged, and optionally skipped_profiles_due_to_credits / warning / unresolvable_profiles.
Inputs: linkedin_urls (required), limit, on_behalf_of, posted_within_days, +1 more.
get_luma_eventsGet Luma eventsARead-onlyInspect
Fetches every page (no truncation), so the returned list is the complete
calendar — upcoming and past events alike. Use it to answer "which event
is next", to find an event's id for get_luma_guests, or when a
luma_event_created trigger fires and you need the full event context.
Dict with events array and count. Each event dict is Luma's event object,
the useful fields being: id (starts with "evt-", the key
get_luma_guests takes), name, start_at / end_at (ISO 8601 UTC),
timezone (IANA), url (the lu.ma page), location_type,
meeting_url, visibility, registration_open, require_approval,
spots_remaining, description_md.
Inputs: on_behalf_of.
get_luma_guestsGet Luma guestsARead-onlyInspect
Paginates to exhaustion — the returned list is the complete guest roster, every approval status included (approved, pending approval, waitlisted, invited, declined). Deciding "who hasn't registered yet" from a partial roster produces false negatives that re-nag people who already signed up, so there is deliberately no page or field trimming here.
When this runs in an agent, each guest is also saved and linked to the agent's
Output tab as an agent_search_results person row (keyed by their LinkedIn
identifier when the form captured one, else their email), so the roster
persists without a separate record step. Best-effort: a persist failure
never fails the read. Re-running updates rows in place; a guest the user has
removed from the list stays removed.
Dict with event_id, guests array, count, and (when saved in an agent) a
list summary {list_name, created, updated, total}. Each guest dict is
Luma's guest object, the useful fields being: user_email, user_name,
approval_status, registered_at, invited_at (null when the guest
found the event themselves), registration_answers (the event form's
question/answer pairs — a LinkedIn-URL question shows up here),
utm_source (which outreach channel drove the registration, when the
invite link carried ?utm_source=).
Inputs: event_id (required), list_name, on_behalf_of.
get_mailbox_deliverabilityGet mailbox deliverabilityARead-onlyInspect
Call this when the user asks why their emails land in spam or the junk folder, or before launching a campaign to confirm the sending mailbox is configured to deliver. Each mailbox reflects the last DNS check — Sliq re-checks daily and on connect, so the data is current without asking the user to re-run it.
verdict is the worst of the four record statuses: 'fail' (a record is
misconfigured and may block or junk sends, so is_send_ready is False),
'warn' (advisory — delivery still works), or 'pass' (all four clean). score
is a 0-100 roll-up that score_tier buckets into 'good' / 'needs_work' /
'critical'. Each checks entry carries the per-record diagnostic (what is
wrong) and fix_instruction_markdown (how to fix it, null when that record
passes) — relay these when the user wants to fix a failing record. A
mailbox's deliverability is None when it has never been checked — tell the
user to open that mailbox in Settings and run the DNS check. An empty
mailboxes list means no email account is connected.
{
'mailboxes': [
{
'email': str, 'provider': str, 'is_default': bool,
'deliverability': {
'domain': str,
'verdict': 'pass' | 'warn' | 'fail',
'score': int,
'score_tier': 'good' | 'needs_work' | 'critical',
'is_send_ready': bool,
'last_checked_at': str, # ISO 8601
'dns_provider': str,
'checks': [
{'record_type': 'spf' | 'dkim' | 'dmarc' | 'mx',
'status': 'pass' | 'warn' | 'fail',
'raw_record': str, 'diagnostic': str,
'fix_instruction_markdown': str | None},
...
],
},
},
...
],
}
Inputs: on_behalf_of.
get_mcp_change_callsGet MCP change callsARead-onlyInspect
Inputs: agent_id (required), change_id (required), limit, offset, +1 more.
get_meeting_transcriptGet meeting transcriptARead-onlyInspect
Inputs: meeting_id (required), on_behalf_of.
get_message_experimentsGet message experimentsARead-onlyInspect
Each test carries versions A and B with their template text, sent / invited / accepted /
replied / interested / meeting / other_reply / not_interested counts (interested
includes a booked meeting; meeting, interested-but-not-meeting, other_reply and
not_interested split replied), and a verdict: too_early until each version has 30
sends, then the leader with its probability of scoring higher on the test's metric —
leading, or wins at 95%. A send step's metric is interested. A connection request's is
accepted, the only kind of test whose invited and accepted counts aren't 0 (invited
equals sent there); there an empty version text is a request
sent with no note, and recent requests haven't all been answered yet, so the version accepted
faster reads a little ahead early on. Relay that probability plainly, and say "too early"
when it is; at current volumes a test takes weeks. A send counts the prospect's reply only
when it came before the next message the user sent them on that channel; a reply after that
answers the later message, so the send scores no reply.
The tests (experiments), newest first, as
{experiments: [{node_id, node_label, metric, started_at, ended_at (null while running), ended_by, versions: [{label, template_id, text, sent, invited, accepted, replied, interested, meeting, other_reply, not_interested}], verdict}]}, where
ended_by says how a past test ended — keep_a / keep_b when that
version went on alone, edit_a / edit_b when that version was edited (which restarts
the test), edit otherwise — and is null while it runs; across every agent each also carries
agent_id, agent_title and owner_email. The experiment at experiment_index also
carries each version's messages (newest first: {prospect_id, prospect_name, person, sent_at, text, outcome: {invited, accepted, replied, interested, meeting, other_reply, not_interested}, reply_text}, reply_text being the reply that answered the send, null
when none did), paged 50 at a time from offset, with
messages_total; invited and accepted are true only on a connection request. A past test that sent
nothing is left out, so an empty experiments means no test in that scope is running or
ever sent a message.
Inputs: offset, node_id, agent_id, on_behalf_of, +1 more.
get_message_tag_ratesGet message tag ratesARead-onlyInspect
Inputs: group_id (required), origin, channel, senders, +2 more.
get_message_tag_unclassified_countGet message tag unclassified countARead-onlyInspect
Inputs: group_id (required), on_behalf_of.
get_node_historyGet node historyARead-onlyInspect
Reads the append-only position log directly, so an off_sequence node id a later flow
edit dropped still returns its history. A node no one has moved off — including the start
node, which stores no position rows of its own — comes back empty.
Dict with node_id, count, and prospects array. count is how many prospects went through and
moved on in total (independent of limit/offset, so the page count is
ceil(count / limit)); prospects is the requested page, newest visit first, each
{id, name, email, linkedin_slug, provider_id, current_node_id, visited_at} —
current_node_id is the node they rest on now, visited_at the ISO time they last
held node_id.
Inputs: node_id (required), agent_id (required), limit, offset, +1 more.
get_outreach_approvalGet outreach approvalARead-onlyInspect
Inputs: agent_id, on_behalf_of.
get_pending_approvalsGet pending approvalsARead-onlyInspect
Each pending row is bucketed by action-type category exactly as the page groups
them: LinkedIn connection requests / messages / follows / post engagement, emails,
gifts, Instantly handoffs, and manual actions. Same-post
comment+reaction rows collapse into one engagement item, matching the page.
A gift item carries the order its card shows (product_url, max_price_usd,
shipping_address, with the note as its summary) and an error; the user
completes any missing piece on the card before approving it. max_price_usd is the
most the item may cost including tax and shipping; approving charges the owner's card
for gifts the order's actual total, at most max_price_usd, plus about $3.30 + 2.9% in
fees on top. error is blank
unless the user approved the card and the order was refused: it then gives the
reason, and the card waits for the user to fix the order and approve it again.
recipient_entered_address is true when the prospect entered shipping_address on a gift
link the user sent; the user's approval then orders the gift to it.
owner_has_gift_card is false when the agent's owner has saved no card for gifts;
approving then orders nothing until the owner adds one under Settings → Billing.
An instantly item is a prospect a campaign flow's Instantly step hands to an Instantly
campaign once the user approves it; its summary gives their address and the Instantly
campaign's id. A LinkedIn, email or instantly item may carry hold_reason, why it's held
beyond the user's approval settings, and teammate_contacts, each teammate who tracks the
person, has queued outreach to them, or has contacted them through Sliq, as its state says.
A LinkedIn or email item whose send waits for the user's working hours on the prospect's clock
carries prospect_hours: that timezone, and the hours the send can go out there (inside both
the user's hours and the prospect's) as start_minute/end_minute past midnight there. Without
it, the send keeps to the user's own hours.
Sequence-produced items (LinkedIn, email, and a manual, gift or Instantly step queued by its
flow node) carry node_id/node_label naming the campaign-flow node that
authored them — null for items created outside a sequence.
A manual item also carries completion_criteria: how to advance the prospect once
the user reports what they did. Cross-agent for the owner; for one agent's items,
pass agent_id, and for one step of its flow, also pass that step's bucket and
keep the items carrying its node_id.
Bounded by default so an approval-gated campaign holding thousands of drafts
doesn't return every full message. The default gives per-bucket/per-agent counts
plus a short preview per bucket (first few items); pass bucket to page that one
bucket's full item list. A bucket's preview_truncated (or count exceeding the
preview) tells you to drill in.
By default (no bucket), a dict
{'pending': {'total': int, 'buckets': [{'bucket', 'title', 'count', 'preview_truncated': bool, 'agents': [{'agent_id', 'agent_title', 'count'}], 'preview': [item, ...]}]}, 'recently_resolved': {'window_days': int, 'total': int, 'items': [...]}}. The preview holds the
bucket's first few full items. Recently-resolved lists only rows with a
transition timestamp — manual done/dismissed, LinkedIn/email sent;
cancelled or rejected drafts don't appear there.
Drill-down (bucket set): {'bucket', 'title', 'count', 'offset', 'limit', 'truncated': bool, 'items': [item, ...]} — full-text items, newest-first;
count is the whole bucket, truncated means more beyond this page (re-call
with offset += limit).
Inputs: bucket, offset, agent_id, on_behalf_of, +1 more.
get_personalization_guideGet personalization guideARead-onlyInspect
Return the user's personalization guide — how they want a person researched before you write to them. Call this only when you are going to personalize a message; follow the returned guide. When none is set, returns a note and you should fall back to the default research rules in the outreach craft skill.
Inputs: on_behalf_of.
get_run_transcriptGet run transcriptARead-onlyInspect
Use this when a run's one-line summary isn't enough (e.g. "why did you skip
person X in that run?"). Get a run's id from get_agent_details — each entry
in its recent_runs list carries one; this returns the same message parts the
web Activity tab renders for that run.
The transcript is paginated over its message parts — a heavy run records many.
Page with offset while has_more is true.
Dict with run_id, the run's summary message + details, status
('completed'/'failed'), created_at, total_messages (parts in the whole
run), returned_messages (parts in this page), offset, has_more, and a
messages array (role, content or tool_name/args, created_at) sliced to
[offset, offset+limit). Empty messages and total_messages 0 when the run
recorded no audit session (e.g. it failed before the agent started).
Inputs: run_id (required), limit, offset, on_behalf_of.
get_segment_funnelGet segment funnelARead-onlyInspect
Inputs: agent_id (required), group_id, on_behalf_of.
get_segment_ratesGet segment ratesARead-onlyInspect
Inputs: group_id (required), channel, senders, agent_ids, +1 more.
get_segment_unclassified_countGet segment unclassified countARead-onlyInspect
Inputs: group_id (required), on_behalf_of.
get_settingsGet settingsARead-onlyInspect
Inputs: section (required), on_behalf_of.
get_skill_guideGet skill guideARead-onlyInspect
Return the best-practice guide for a Sliq workflow — read and follow it
before acting on that workflow. skill selects the guide.
Inputs: skill (required), on_behalf_of.
get_tag_cross_tabGet tag cross tabARead-onlyInspect
Inputs: col_kind (required), row_kind (required), col_group_id (required), row_group_id (required), +5 more.
get_tool_connect_urlGet tool connect URLARead-onlyInspect
If the user says a service is already connected, call get_integration_status first — the
service may already be active and need no link.
A link that opens fine but whose connection then fails is not a link problem — a fresh link routes
to the same connect flow and fails the same way. Mint one fresh link and have the user retry once; if
it still doesn't connect, the connection is stuck (not a link issue) — call escalate_to_team with the
integration and what the user tried rather than re-minting again.
For linkedin, gmail, and outlook the return also includes data_backfill — a sentence stating
how far back Sliq syncs history once connected. Relay it with the link so the user knows older
data won't appear and how to request a longer backfill; when sharing several connect links in
one message, merge the notes into a single short disclaimer instead of repeating one per link.
Dict with the connect URL, integration name, the data_backfill note
(linkedin/gmail/outlook), and connect_note (linkedin)
Inputs: integration_id (required), on_behalf_of.
get_user_memoryGet user memoryARead-onlyInspect
The user's saved memory — the durable facts, preferences, and context Sliq has learned about them across conversations. Read it to ground a reply in what's already known before asking the user to repeat something. It is account-wide (shared across all their agents), not per-agent. When none is saved yet, returns a note saying so.
Inputs: on_behalf_of.
get_writing_style_guideGet writing style guideARead-onlyInspect
Return the user's writing style guide for one channel, to follow when drafting that channel's outreach. Pass channel='linkedin_dm' for a DM or connection note, or channel='email' for an email. A set guide is the user's own voice and overrides the default craft rules; returns a note when none is set.
Inputs: channel (required), on_behalf_of.
google_news_searchGoogle news searchARead-onlyInspect
A find-companies News-mode discovery source, used alongside
search_news_events, tavily_search, and exa_search_news — the
News-mode skill owns the routing (which signals fan out here). Its
edge is Google's local and trade-press index: hyper-local expansion
stories, plant incidents within hours of publication, and
small-market executive moves the other sources' indexes miss.
Returns raw articles, not company rows — extract the company from
each article's title/snippet the same way as tavily_search results.
Compose KEYWORD-style queries — the terms a journalist's headline would contain, e.g. 'appoints new chief marketing officer' or 'breaks ground manufacturing facility'. Google matches keywords, not meaning: full-sentence semantic phrasing ("A company that recently...") returns ZERO results. Geographic scoping via keywords is brittle (a location term matches any article containing the word, wherever the company is) — prefer scoping by event keywords and filtering location downstream. One query per signal, all in one call (the tool fans them out in parallel; one tool call is one LLM round).
Expect the same story to arrive several times from different
outlets — Google clusters dilute a page's effective yield to ~5–7
distinct companies. Dedup is on exact URL here; cross-outlet
same-story dedup happens downstream at record_search_results.
Articles are recency-bounded server-side by Google's coarse
day/week/month/year buckets, using the smallest bucket covering
lookback_days (default 30) — a bucket can exceed the window, so also
check each row's published_at when the window matters. A news scan
passes the window from its trigger prompt.
Costs 0.05 Sliq credits per query that returns without error; a
failed query is free. Companies you record from its articles are not
charged. Soft-fails on insufficient credits: the charge is capped at
the remaining balance.
Dict with articles array and count. Each article row is
{title, url, source, snippet, date, published_at, query} —
source is the publisher name, date is Google's relative
publish string ("2 days ago"), published_at is a coarse UTC
ISO estimate derived from it (None when unparseable), and
query names the originating query. Rows are deduped by URL
across queries.
If every query failed with an upstream error and nothing
landed, also google_news_available: False plus a
human-readable error — a source outage, NOT a genuinely
empty feed. Partial failure (some queries errored, others
returned) stays silent and reports only the articles.
On insufficient Sliq credits the call is refused before running —
it raises InsufficientCreditsError instead.
Inputs: queries (required), on_behalf_of, lookback_days.
import_linkedin_historyImport LinkedIn historyAInspect
Inputs: window (required), on_behalf_of.
list_agency_clientsList agency clientsARead-onlyInspect
The client accounts the caller works as an agency, their own and their teammates',
with each client's credit balance and Slack reply alerts and each person's LinkedIn
sending state. Pass any member's email but your own as on_behalf_of on a Sliq tool to
run that call in their account.
Returns {'groups': [{'key', 'name', 'operator_email', 'members': [{'email',
'display_name', 'linkedin'}], 'credits': {'available', 'level' (0 ok, 1 low, 2 almost
out)} | None, 'reply_alerts'}, ...]}. The first group (key 'team') is the caller's own
team, with operator_email and reply_alerts None. On every other group
operator_email is the client's main operator, and reply_alerts is which of the
members' LinkedIn replies post to the agency's Slack channel: 'all', 'interested' (a
reply classified interested or meeting booked, or any reply from a prospect already at
meeting booked), or 'off'. A member's linkedin is
None when they have no LinkedIn account connected, else {'linkedin_reason'
('needs_reconnect' | 'auto' paused by LinkedIn limits until linkedin_until |
'manual' paused by the user | None sending), 'linkedin_until',
'connection_request_until' (connection requests alone held until then, or None)}.
A caller who works no clients gets only the 'team' group, or {'groups': []} with no
teammates either.
Inputs: on_behalf_of.
list_agentsList agentsARead-onlyInspect
Inputs: on_behalf_of.
list_attachmentsList attachmentsARead-onlyInspect
List the files in the user's attachment library — id, filename, type, size.
Defaults to global company-context files plus files belonging to the current agent or chat. Set all_agents=True to list the user's whole library across agents. Use a returned id with read_attachment or attach_to_outreach.
Inputs: all_agents, on_behalf_of.
list_classifiable_campaignsList classifiable campaignsARead-onlyInspect
Inputs: on_behalf_of.
list_companiesList companiesARead-onlyInspect
Each row: id (the company id query_companies uses, and the company_profile_id on
query_people and query_search_results rows), display_name, domain, spine (industry,
location, employee_count, description and the other firmographics), and:
people_countandpeople_preview: the people the user knows at the company, across the account, not only the ones an agent found. To list them with their outreach, call list_people withcompany_idand noagent_id.signals: {count, latest, monitored}: how many signals the company has, the newest one, and whether a monitor watches it. Withagent_id, these are this agent's (from its searches and its monitoring); without it, every agent's search finds in the account plus the user's own monitors.signal_entries, only withinclude_signals: every one of those signals, newest first.
With agent_id, each row also has:
listsandsources: the search lists and discovery tools that surfaced the company, empty for one the agent only monitors.columnsholds the researched [label, value] pairs.verdict,verdict_reasonandrejected_on: the curation recorded on the company's search rows, as the Verdict column shows it. 'rejected' when every row is rejected, 'qualified' when any row is qualified, else 'unclear' (screened but too thin to judge) when any row is, else null, with the reason from the latest row carrying it. On agroup_by='list'bucket page, or under a "run" filter whose runs all saved to one list, it's the verdict on that list's row.rejected_onis {lists, reason}: the lists whose own verdict is 'rejected' while the company'sverdictisn't, with the latest rejected row's reason; itslistsis empty otherwise and on those two reads. A company rejected on every row that the agent doesn't monitor is left out unless a "verdict" filter asks for 'rejected', a "run" filter is set orinclude_rejectedis set, the same as the user's default view.criteria, only withinclude_criteria: why the agent matched the company, as its opened row shows it. Per list it's in, {list_name, evaluations}; each evaluation is acriterionwith thereasoning, a verdict (satisfied'yes', 'no' or 'unclear', or a numericscorewhere 7 and up is met) and http(s)references.
Criteria run a few KB per company and signal entries usually under one, so ask for either on a
narrow read (a q, one bucket, or a small limit) or from run_code.
The "list" and "source" groupings, the "list", "source", "verdict", "run" and researched-column
filters, sort='latest_find', include_criteria and include_rejected read one agent, so
they need agent_id.
To count companies per group, pass group_by alone. To list one group's companies, pass
group_by plus a bucket key taken from those counts. Otherwise you get every company, in the
order sort picks. Page until next_cursor is null by passing it back as cursor. A direct
call returns 25 rows by default; for a wide pull, call this from run_code with limit up to 200
(nothing truncates there) and print only the fields you need.
A dict. A group_by without a bucket returns group_counts, a list of {key, count}
with the largest group first and the '' group last, and total, the matching companies
each counted once. Every other call returns results (rows shaped as above) and
next_cursor, plus total (every matching company) when group_by is omitted. With
agent_id, every call also returns filtered_out_count: how many companies this same call
matches once a "verdict" filter on 'rejected' is added to filters, with the same q and
group_by and no bucket, each company counted once. Only under the same search, filters
and grouping as the user's Companies table does it equal the number that table shows as
"didn't match".
Inputs: q, sort, limit, bucket, +8 more.
list_inbox_conversationsList inbox conversationsARead-onlyInspect
Inputs: tab, limit, offset, agent_id, +1 more.
list_message_tag_groupsList message tag groupsARead-onlyInspect
Inputs: on_behalf_of.
list_pagesList pagesARead-onlyInspect
Inputs: archived, on_behalf_of.
list_peopleList peopleARead-onlyInspect
Each row: id (the person id query_people uses), display_name, spine (title, company,
location, headline), company_profile_id (their company's list_companies id, or null),
linkedin_slug, linkedin_provider_id, email, outreach_stage and removed. Removing
someone from a campaign leaves their outreach_stage as it was.
With include_signals it also has signal_entries, the signals the person's profile shows,
newest first and the same with or without agent_id: their company's, and the one each of
their campaigns enrolled them on. Each entry's company_id and company_name are the company the
signal belongs to, which can differ from the person's own; they are null for an enrollment
signal no company row holds. They cost a few database reads per person, so ask for them on a narrow read (a
q, one bucket, or a small limit).
Without agent_id, each row also has:
outreach_stage: the person's most advanced lead-funnel bucket across the campaigns of the user and every teammate, '' when they're in none.removed: true when they were removed from every one of those campaigns they're in.activity: those campaigns' prospect rows on the person, newest first, each {owner_email, owner_display_name, agent ({id, title}, or null), stage, removed, last_touch_at}, whereremovedis whether they were removed from that campaign.
With agent_id, each row also has:
degree: the user's LinkedIn degree to the person, '1', '2', '3' or 'out_of_network'; null when unknown. A person the user is connected to reads '1'.intro: the warm-intro check.stateis one of not_checked, checking, queued, found, cold, connected, unresolved.connectorslists the user's 1st-degree connections who know the person (name, headline, identifier, provider_id).mutual_connections_truncatedtrue meansconnectorsis a sample of a longer list.shared_connections_countis LinkedIn's total.result_idis the search row the check read, null when the person has none.outreach_stage: the person's lead-funnel bucket on this agent, '' when not in outreach here.prospect_id,email_stageandlinkedin_stageare their prospect row on this agent, null when not enrolled.removedis true when they were removed from the agent's campaign, andremoval_reasonsays why, '' when they weren't or no reason was recorded.listsandsources: the search lists and discovery tools that surfaced the person.columnsholds the researched [label, value] pairs.verdict,verdict_reasonandrejected_on: the curation recorded on the person's search rows, as the Verdict column shows it. 'rejected' when every row is rejected, 'qualified' when any row is qualified, else 'unclear' (screened but too thin to judge) when any row is, else null, with the reason from the latest row carrying it. On agroup_by='list'bucket page, or under a "run" filter whose runs all saved to one list, it's the verdict on that list's row.rejected_onis {lists, reason}: the lists whose own verdict is 'rejected' while the person'sverdictisn't, with the latest rejected row's reason; itslistsis empty otherwise and on those two reads. A person rejected on every row is left out unless a "verdict" filter asks for 'rejected', a "run" filter is set orinclude_rejectedis set — the same as the user's default view — though a tracked prospect always shows.criteria, only withinclude_criteria: why the person matched, as their opened row shows it. Per list they're in, {list_name, evaluations}; each evaluation is acriterionwith thereasoning, a verdict (satisfied'yes', 'no' or 'unclear', or a numericscorewhere 7 and up is met) and http(s)references. Criteria run about 2KB per person, so ask for them on a narrow read (aq, one bucket, or a smalllimit) or from run_code. A list where all of the person's rows are rejected is left out unless the read includes rejected people.
The "list", "source", "degree" and "connector" groupings, the "list", "source", "degree",
"verdict" and "run" filters, include_criteria and include_rejected read one agent, so they
need agent_id.
To count people per group, pass group_by alone. To list one group's people, pass group_by
plus a bucket key taken from those counts. Otherwise you get everyone, sorted by name. Page
until next_cursor is null by passing it back as cursor. A direct call returns 25 rows by
default; for a wide pull, call this from run_code with limit up to 200 (nothing truncates
there) and print only the fields you need.
A dict. A group_by without a bucket returns group_counts, a list of
{key, count} with the largest group first and the '' group last ("degree" keeps
closest-first order and "segment" its group's tag order then 'No match'; "connector"
entries add label), and total, the matching people
each counted once. Every other call returns results (rows shaped as above) and
next_cursor, plus total (every matching person) when
group_by is omitted. With agent_id, every call also returns filtered_out_count: how
many people this same call matches once a "verdict" filter on 'rejected' is added to
filters, with the same q, group_by and segment_group_id and no bucket, each person
counted once. Only under the same search, filters and grouping as the user's People table
does it equal the number that table shows as "didn't match".
Inputs: q, limit, bucket, cursor, +9 more.
list_prospect_eventsList prospect eventsARead-onlyInspect
Spans every campaign by default; pass agent_id to scope to one. To page further
back, re-call with before set to the oldest returned event's occurred_at.
Dict with count, a summary block (replies_this_week, accepts_this_week,
posts_found_this_week, meetings_booked_this_week), and an events array. Events are newest-first; each is
{id, kind, channel, owner_email, occurred_at, time_precision, subject_type, snippet,
turn, prospect: {id, name, linkedin_slug, provider_id, title, company,
has_conversation}, agent: {id, title} | null, queued_followup: {...} |
null}. turn is 'user' when a draft awaits the user's approval, 'sliq'
when a send is queued, else 'none'; queued_followup carries that drafted
next step when one exists. prospect.id is null when the event isn't
attributable to a tracked row.
Inputs: kinds, limit, before, agent_id, +1 more.
list_segment_groupsList segment groupsARead-onlyInspect
Inputs: on_behalf_of.
list_sent_messagesList sent messagesARead-onlyInspect
Inputs: q, limit, offset, origin, +7 more.
list_teammatesList teammatesARead-onlyInspect
The caller's active-workspace teammates. Pass an active teammate's email as on_behalf_of
on a Sliq tool to run that call in their account.
Returns {'teammates': [{'email', 'display_name', 'role' ('admin'|'member'), 'status' ('invited'|'active'), 'is_you', 'joined_at', 'invited_by'}, ...]}, ordered by join time. Includes the caller (flagged 'is_you'). A solo user with no workspace gets {'teammates': []}.
Inputs: on_behalf_of.
look_up_imported_peopleLook up imported peopleAInspect
Inputs: senders, on_behalf_of.
manage_email_outreach_queueManage email outreach queueADestructiveInspect
Bulk operations (status, approve-all, cancel_all) act on the current agent's queue when run inside an agent, and across the whole account from account-level chat — except a bulk cancel_all or approve-all, which from account-level chat is rejected unless agent_id names the campaign, so it can't hit every campaign at once. Targeting one person by recipient_email reaches them in any agent. The next send time and per-mailbox usage stay account-wide (the send rate is per-mailbox).
For pausing/resuming the campaign, use update_agent to set the agent status to 'paused' or 'active' instead. Dict with action result and current queue status
Inputs: action (required), body, offset, subject, +5 more.
manage_inboxManage inboxCInspect
Inputs: action (required), mailbox, provider, email_ids, +4 more.
manage_linkedin_invite_queueManage LinkedIn invite queueADestructiveInspect
Bulk operations (status, approve-all, cancel_all) act on the current agent's queue when run inside an agent, and across the whole account from account-level chat — except a bulk cancel_all or approve-all, which from account-level chat is rejected unless agent_id names the campaign, so it can't hit every campaign at once. Targeting one person by target_provider_id reaches them in any agent. Pacing / next_scheduled / daily usage stay account-wide (the LinkedIn rate limit is per-account). Dict with action result and current queue status
Inputs: action (required), offset, message, node_id, +10 more.
mark_monitor_events_notifiedMark monitor events notifiedAInspect
Mark monitor events as included in a digest notification. Call this after composing a Slack digest to prevent re-notifying the same events. Only affects events belonging to the current user; in an agent run it scopes to the run's agent.
Inputs: event_ids (required), on_behalf_of.
matches_icpMatches ICPARead-onlyInspect
Runs the two mechanical checks — employee-count band and funding-stage
normalized membership, both include-on-null — and combines them with the
caller's industry and location judgments. Industry fit and location are NOT
decided here: the agent judges both and passes its verdicts in.
{passed, location_ok, size_ok, size_null_included, size_contested,
industry_ok, funding_stage_ok, funding_stage_null_included}. passed is
the AND of location_ok, size_ok, industry_ok, funding_stage_ok.
Size is not a pure hard gate — it includes-on-uncertain-data like funding, because Apollo undercounts privately-held / industrial firms:
size_null_includedTrue → Apollo had no headcount; included anyway.size_contestedTrue → Apollo's count is below the smallest band floor (probably an undercount); included anyway. Either flag → score that Company-size evaluation 5 (amber "verify"), not
A count in a gap between bands or above the ceiling is a confident mismatch → size_ok False (drop). Score Industry from
industry_verdict(fit → 10, uncertain → 5) and Location fromlocation_verdict(in → 10, uncertain → 5).
Inputs: industry_verdict (required), location_verdict (required), icp_employee_count_ranges (required), on_behalf_of, +3 more.
move_prospect_to_nodeMove prospect to nodeAInspect
Inputs: node_id (required), agent_id (required), prospect_id (required), on_behalf_of.
query_analyticsQuery analyticsCRead-onlyInspect
Inputs: metric (required), days, custom_query, on_behalf_of.
query_companiesQuery companiesARead-onlyInspect
Columns: id, display_name, domain (normalized bare host), linkedin_company_id, owner_email,
data (JSONB), created_at, updated_at. Sliq's CRM is deals and tasks: to see whether a company
has a deal or a task, use query_deals (status='all') / query_crm_tasks with company_id.
JSONB queries: data->>'some_key' ILIKE '%...%'.
Firmographic columns (captured from company enrichment; blank/NULL until enriched): linkedin_slug (the LinkedIn company page's lowercase slug: 'acme-corp' for linkedin.com/company/acme-corp), industry, description, location (HQ), logo_url, employee_count, follower_count, founded_year, company_type, annual_revenue, total_funding, latest_funding_stage.
To list the people you know at a company, take an id from here and call query_people with
where_clause="company_profile_id = <id>".
For how many companies are in each industry or location, or which companies are in one of those groups, call list_companies: it reads the Companies table the way the user sees it, over every company without paging for the counts. A dict with count, truncated, and items array (each row {id, display_name, domain, linkedin_company_id, owner_email, linkedin_slug, industry, description, location, logo_url, employee_count, follower_count, founded_year, company_type, annual_revenue, total_funding, latest_funding_stage, data, created_at, updated_at}).
Inputs: limit, offset, order_by, on_behalf_of, +1 more.
query_crm_tasksQuery CRM tasksARead-onlyInspect
The follow-ups Sliq suggested to the user (the Tasks view's suggested follow-ups, waiting
for the user to add or dismiss them) come only with status='suggested', and the ones they
dismissed only with 'dismissed', newest dismissal first; both cover only the user's own
suggestions. A suggestion's data.trigger says why Sliq made it: 'stalled_conversation'
(the person went quiet after the user's last message), 'follow_up_requested' (they asked to
be contacted later) or 'stage_stalled' (its deal has sat in one stage too long).
Dict with paging info and the tasks page.
{total, has_more, tasks: [{id, title, notes, due_at, status, completed_at,
assignee_email, person_profile_id, company_profile_id, deal_id, person_profile,
company_profile, deal (the linked deal, or null; its open_task_count and next_task
aren't filled in here — use query_deals for those), data, created_at, archived_at}]}.
Inputs: q, limit, offset, status, +6 more.
query_dealsQuery dealsARead-onlyInspect
Every result also carries the pipeline's stages (in order) and a pipeline_summary
(open count and amount, closing this month, won/lost counts, win rate) for the whole
pipeline — the summary ignores the filters.
Dict with paging info, the stages, the pipeline summary, and the deals page.
{total, has_more, stages: [{id, name, position}], pipeline_summary: {open_count,
open_amount, closing_this_month {count, amount}, won_count, lost_count, win_rate},
deals: [{id, name, amount (decimal string or null), status, stage {id, name,
position}, person_profile {id, name, title, email, linkedin_slug, ...} or null,
company_profile {id, name, domain} or null, assignee_email, expected_close_date,
notes, created_at, stage_changed_at, closed_at (when it was won or lost; null while
open), archived_at, open_task_count, next_task {id, title, due_at} or null,
suggested_task, others_at_company {company {id, name}, count, other_open_deal} or
null (how many other people at the deal's company have had outreach sent or are on a
deal there, and whether another open deal sits there), data}]}.
Inputs: q, limit, stage, offset, +5 more.
query_linkedin_post_engagementsQuery LinkedIn post engagementsARead-onlyInspect
Use this to find warm leads, identify who engaged with specific posts, or filter engagers by role/company. Chain with query_linkedin_posts when drilling into a specific post, or filter on the post_analytics_id that fetch_post_engagers returns ("post_id = 42").
Columns: id, post_id, engagement_type ('reaction'|'comment'), reaction_value (LIKE|ENTERTAINMENT|EMPATHY|PRAISE|INTEREST|APPRECIATION, empty for comments), author_name, author_provider_id, author_headline, author_profile_url, comment_text, comment_id, parent_comment_id, reply_count, is_post_author, engaged_at, fetched_at, created_at.
A 'comment' row is a comment on the post or a reply to one. A reply's parent_comment_id is the comment_id of the comment it answers; a comment's is ''. reply_count is how many replies the comment has on LinkedIn, so a comment with more replies than reply rows here has replies that weren't captured. is_post_author marks rows the post's author wrote, so on the user's own post a reply with is_post_author true is the user's own answer. To read a post's threads, filter "post_id = 42 AND engagement_type = 'comment'". To reply to a comment or like it, pass its comment_id as parent_comment_id to queue_linkedin_post_engagement. A comment with a blank comment_id can't be replied to until fetch_post_engagers fetches the post again. Dict with count, truncated, and engagements array (each row carries the columns above).
Inputs: limit, offset, order_by, on_behalf_of, +1 more.
query_linkedin_postsQuery LinkedIn postsARead-onlyInspect
Use this to answer "which of my posts performed best?", "what's my average engagement?", "show me posts from last month", etc. This is the account-global table, with no agent/monitor dimension — for the feed a specific agent's monitors discovered (and to act on it), call query_monitored_posts.
Columns: id, social_id, text, share_url, posted_at, impressions, reactions, comments, reposts, is_own_post, author_name, author_headline, fetched_at, created_at, plus a derived engagement_rate string (reactions+comments+reposts over impressions, matching the UI) — '0.0' for posts with no impression data (e.g. third-party posts). The engagement_rate is computed after the query runs, so it can't appear in where_clause or order_by; filter and sort on the raw columns instead. Dict with count, truncated, and posts array (each row carries the columns above plus the derived engagement_rate).
Inputs: limit, offset, order_by, on_behalf_of, +1 more.
query_monitored_companiesQuery monitored companiesARead-onlyInspect
mode="search" (default): individual company rows. mode="stats": aggregate counts, with optional group_by for analytics.
Columns: display_name, domain, stage ('active'/'removed'), data (JSONB),
company_profile_id (the company's id in query_companies; null for a company
tracked without a usable domain, which the Companies view leaves out), agent_id,
created_at, updated_at. JSONB queries: data->>'last_sweep_status' = 'error'.
For outreach prospects, call query_prospects.
For the agent's Companies view as the user sees it, each company with this agent's
signals and how many of the user's people work there, call list_companies.
To list the user's people at monitored companies, call query_people with
where_clause="company_profile_id IN (<ids>)". list_people with this agent's id holds
only the people this agent found, not everyone the user knows at the company.
In search mode, {count, truncated, items array}.
In stats mode, {total, by_stage: {active: N, removed: M},
groups?: {: {total, by_stage}}}.
Inputs: mode, limit, offset, agent_id, +4 more.
query_monitored_postsQuery monitored postsARead-onlyInspect
mode="search" (default): individual post rows. mode="stats": per-status counts.
Each row is one post a monitor surfaced, with the comment Sliq drafted for it
(draft_comment) and the post's lifecycle status: 'discovered' (claimed,
pre-draft), 'filtered' (the monitor's post filter set it aside, so no action ran
on it), 'queued' (drafted + enqueued), 'commented' (comment sent),
'rejected' (user declined the draft), 'skipped' / 'skipped_no_credit' (no
comment drafted), 'captured' (a capture ran on it — its engagers, or its
author when a person — and it wasn't commented), 'surfaced' (no comment and no
capture ran — a surface-only monitor, or a company page's or the user's own post
on an author-only monitor — just surfaced in the feed to act on manually).
author_captured is whether author capture ran on the post's author, a person;
the monitor's list holds them unless the user removed them.
filter_reason is the post filter's one-sentence reason for keeping or setting
aside the post ('' when the monitor has no filter). Read it beside the filtered
and kept posts when the user wants to tune their monitor's post filter.
data holds the author's headline and public_identifier. Link a post's author
with the derived author_profile_url on each search row — a person's profile, or
the company page when a company posted. It is computed after the query runs, so it
can't appear in where_clause or order_by.
reaction_status is the independent reaction track: '' (no reaction),
'queued' (a reaction enqueued), 'reacted' (sent), 'rejected' (user declined it),
'skipped' (deduped) — disjoint from status since one post can be both
commented and reacted to. reaction_type is which reaction landed
(like/celebrate/support/love/insightful/funny, '' until sent).
Columns: id, monitor_id, post_urn, post_url, author_name, author_provider_id,
data, post_text, reaction_count, comment_count, posted_at, status, filter_reason,
draft_comment, reaction_status, reaction_type, author_captured, created_at. Pass a row's id to
generate_monitored_post_comment to draft + queue a Sliq comment for it.
Pass agent_id to scope to that agent's monitors — omitting it reads every
monitored post across all the user's agents. To act on recent posts (e.g. queue
a reaction on this agent's fresh finds), pass agent_id and filter on
posted_at; this keeps actions on the agent's own scoped feed rather than the
account-wide analytics.
This is the monitor-scoped feed. query_linkedin_posts is a different table —
the account-global analytics of the user's own posts plus any post they've
fetched engagers for, with no agent/monitor dimension. Use that for "how did my
posts perform"; use this for "what did this agent's monitors find".
In search mode, {count, truncated, items array}.
In stats mode, {total, by_status: {discovered: N, commented: M, ...}}.
Inputs: mode, limit, offset, agent_id, +3 more.
query_peopleQuery peopleARead-onlyInspect
Columns: id, display_name, title, headline, company, location, timezone,
summary, linkedin_slug, linkedin_provider_id, email, mail_host, connections_count, follower_count,
is_open_profile, is_premium, work_experience (JSONB), education (JSONB), linkedin_looked_up_at,
company_profile_id, owner_email, data (JSONB), created_at, updated_at. company_profile_id is
the person's canonical employer FK (NULL when unresolved). Sliq's CRM is deals and tasks: to
see whether someone has a deal or a task, use query_deals (status='all') / query_crm_tasks
with person_id.
mail_host is the email provider hosting this person's address (Google Workspace,
Microsoft 365, …), read from their domain's MX record: google, microsoft, gateway,
other, unknown (no mail record, or the domain's DNS is broken), or '' with no email or
no successful lookup yet.
gateway means a security gateway (Mimecast,
Proofpoint) receives the mail first, which hides the provider behind it.
timezone is the IANA time zone the person is in (such as America/Los_Angeles): one set
by hand, else the one their location names. It is '' when unknown: they have no
location, it names no single zone (a country spanning several), or it isn't classified yet.
For a prospect, set it with update_prospect's timezone when you know where they are.
connections_count / follower_count are NULL when never fetched (not 0); is_open_profile /
is_premium are NULL likewise. JSONB queries: data->>'some_key' ILIKE '%...%'.
work_experience and education are the person's full LinkedIn employment and education
history — the same arrays enrich_linkedin_profiles returns, already stored on the person from
the last profile lookup, so reading them here costs nothing (answer "where did they go to
school", "are they an LSU alum", "how long in seat" from these instead of a fresh enrich).
Each is a list, in LinkedIn display order (NOT sorted by date). linkedin_looked_up_at is when
the person's full LinkedIn profile was first looked up: NULL means it never was, and a time
with empty lists means LinkedIn had no history to give. These arrays are large for senior
people — a broad limit=200 read that returns them can exceed the 50KB direct-return cap and
truncate; for a wide pull, either narrow the where_clause or call this from run_code
(nothing truncates there) and print only the fields you need.
To list the outreach prospects tracking a person, take an id from here and call
query_prospects with where_clause="person_id = <id>".
Each row also carries segments: the person's segment tags across every segment group they're
classified into — [{group_id, group_name, tag}], tag 'No match' where the classifier couldn't
place them (empty when no group has classified them). Segments are the LLM-defined
people dimensions from the Explore Segments surface (e.g. Seniority -> VP); manage them with the
segment tools (list_segment_groups etc.).
Each row also carries degree: the user's LinkedIn degree to the person, '1', '2', '3' or
'out_of_network', null when unknown — from the user's synced LinkedIn connections and what
every one of their campaigns' searches and lookups recorded. A person the user is connected to
reads '1'. It is the degree the person's profile shows beside their name.
For how many people are in each title, company, location, outreach stage or segment, or who is in one of those groups, call list_people: it reads the People table the way the user sees it, over everyone without paging for the counts. A dict with count, truncated, and items array (each row {id, display_name, title, headline, company, location, timezone, summary, linkedin_slug, linkedin_provider_id, email, mail_host, connections_count, follower_count, is_open_profile, is_premium, work_experience, education, linkedin_looked_up_at, company_profile_id, owner_email, segments: [{group_id, group_name, tag}], degree, data, created_at, updated_at}).
Inputs: limit, offset, order_by, on_behalf_of, +1 more.
query_prospect_researchQuery prospect researchARead-onlyInspect
Call this BEFORE researching a lead to avoid re-doing work (and re-spending credits), or to reference prior findings in a follow-up, or to answer "what did you find about X?". Dict with count, truncated, and items array (each row {id, agent_id, person_linkedin_slug, person_linkedin_pid, person_email, subject_kind, company_domain, display_name, content, url, source, created_at}).
Inputs: agent_id (required), limit, offset, person, +1 more.
query_prospectsQuery prospectsARead-onlyInspect
mode="search" (default): individual prospect rows. mode="stats": aggregate counts, with optional group_by for analytics.
Columns: id, person_id, display_name, email, mail_host, linkedin_slug,
linkedin_provider_id, title, headline, company, location, timezone, email_stage, linkedin_stage,
priority (True = this prospect's email and LinkedIn sends jump the queue), removed_at,
current_node_id, entered_at, enact_blocked_at, enact_blocked_reason, data (JSONB), agent_id,
created_at, updated_at, linkedin_messages_sent, last_outbound_at.
There's no rollup stage column; filter on email_stage or linkedin_stage
to isolate a funnel (both non-null — not-started is ''; gate absence with
= ''). JSONB queries: data->>'title' ILIKE '%founder%'. For
monitored companies, call query_monitored_companies.
id is the prospect's outreach_prospects primary key — returned on every
search row and queryable in where_clause. When an event hands you a roster
of prospect primary keys (a flow node's on_enter enactment carries them in
event['prospect_ids']), resolve the whole roster with id IN (...) and order_by="id",
so it queues in upload order.
person_id is the canonical-person FK — the query_people entity this outreach row
resolves to. Each row also carries that person's canonical identity line — title,
headline, company, location — joined from that query_people row: the deduped,
typed, filterable values (the prospect's own data blob may carry a separate
per-enrollment copy under data->>'title'/'company'). For the rest of the profile —
summary, work history, education — call query_people with id = <person_id>.
person_id = <id from query_people> lists every prospect row tracking that person. Use it
to cross between the two: query_people returns one deduped row per real person,
query_prospects returns the per-owner tracking rows under it. These are your own prospects
only — a teammate's rows for the same person need on_behalf_of. For a person's campaigns
across you and every teammate, with their most advanced stage,
call list_people without an agent_id: one row per person carries both.
removed_at is when the person was removed from the campaign (update_prospect with
stage='skipped', the user's Remove, or a cross-campaign duplicate), NULL while they're in
it. A removed person gets no further sends, and their stages keep what already happened,
so removed_at IS NULL is the filter for people still in the campaign.
Each search row also carries lead_bucket, the derived lead-funnel bucket
(one of not_started/queued/contacted/engaged/replied/interested/meeting_booked/
not_interested/finished/not_reached/already_connected): the furthest stage the person
reached. It's a read-only returned field, not a queryable column. For a
funnel rung's people — everyone who got at least that far on either channel, removed
people included, as an agent's lead_funnel.reached counts them — call list_people with
the agent_id and a "reached" filter for the rung. To filter by one stage, gate on
email_stage/linkedin_stage, never on lead_bucket. Each row also carries a read-only
removal_reason: for a removed prospect, why they were taken out of the campaign; ''
otherwise or when no reason was recorded.
current_node_id / entered_at are the prospect's Campaign Flow position — the
sequence-DAG node they currently rest on and when they arrived. current_node_id is
NULL for a prospect on the start node / not yet routed onto any node. The node id is
opaque; resolve it to a label and kind — and read the per-node occupancy counts — with
get_campaign_flow. Filter on it to list one node's occupants
(current_node_id = '<id from get_campaign_flow>', or current_node_id IS NULL for the
start node) or to find stalled prospects (entered_at <= '<7 days ago>').
enact_blocked_at / enact_blocked_reason answer "why did this prospect stop?" —
non-null means their current node's step could not be performed and won't retry
until unblocked (enact_blocked_at IS NOT NULL lists a campaign's stuck
prospects). Fix the cause, then clear the block with retry_blocked_enactment.
A search row for a prospect who has been queued a gift also carries gifts, a read-only
returned field listing every gift to them, oldest first: {id, agent_id, status, cancel_kind, node_id, product_url, tracking_url, error, created_at, placed_at, delivered_at, claim_url, can_send_on_linkedin, can_send_by_email}. agent_id is the
campaign the gift was queued in: usually the row's own, but a gift carried over when
duplicate records of the person merged can belong to another campaign, and it is null once
that campaign is deleted. status is
pending_approval (a card waiting on the user's approval, which is also where a card sent
as a link returns once its recipient enters their address: to tell which, and to read that
address, call get_pending_approvals with bucket='gift' and read the card's
recipient_entered_address and shipping_address), awaiting_claim (the user sent a link
for the recipient to enter their address, and no one has yet),
placing, placed, shipped, delivered, failed (error says why) or cancelled.
claim_url is that link while the gift is awaiting_claim, the one to give the user or put
in a message to the recipient, and null otherwise. can_send_on_linkedin is true when the user
can send that link, after the gift's note, to the recipient on LinkedIn with the Send on LinkedIn
button beside it in Sliq: the link is open, the gift has a note, and the recipient is a 1st-degree
LinkedIn connection of the user. can_send_by_email is true when the user can email that link,
after the gift's note, to the recipient from their own mailbox with the Send by email button
beside it in Sliq: the link is open, the gift has a note, Sliq has an email address for the
recipient that hasn't bounced, and the user has connected Gmail or Outlook. cancel_kind says
why a cancelled gift was cancelled, and is null on every other status: user_rejected (the
user rejected the card or cancelled its link), agent_cancelled (you cancelled it at the
user's request),
prospect_moved (the prospect had already left the gift step when the user approved the
card or sent it as a link), prospect_skipped (the prospect was removed from the campaign), task_deleted (the
campaign was deleted), duplicate (a merge left another gift on the same step) or
unclaimed (no one entered an address within 14 days of the user sending the link).
node_id names the gift step in that campaign's flow, null for a gift queued outside the
flow.
A search row for a prospect a flow's Instantly step has handed to an Instantly campaign also
carries instantly_handoffs, a read-only returned field listing every handoff of them, oldest
first: {id, agent_id, node_id, campaign_id, status, cancel_kind, last_error, created_at, handed_off_at, sent_at, finished_at}. status is pending_approval (a card waiting on the
user's approval), pending (Sliq adds them to the Instantly campaign as a lead within minutes,
unless the handoff's Sliq campaign, agent_id, is paused), dispatching (being added),
active (Instantly's campaign is running the sequence for them; it has emailed them once
sent_at is set), stopping (Sliq stopped pursuing them and settles the lead about an hour
later, removing it from the campaign only while Instantly is still running it there with no
reply and no interest status), completed (Instantly finished the sequence; finished_at
dates that by its last email to them, or by its last update to a lead it never emailed, and is
null on a handoff Sliq had already stopped. Once it's set, while they still rest on the step
and the campaign isn't paused, they move along the step's no_reply when that delay has
passed since finished_at, unless a reply moves them first), replied, bounced, failed
(last_error says why) or cancelled. handed_off_at is when the lead reached Instantly;
a cancelled handoff that has it was removed from the campaign, unless last_error says it
was left there or cancel_kind is unsubscribed. On a handoff still in progress,
last_error is set only when Instantly isn't connected, refused the key, or has no active
paid plan, or when a pending_approval handoff is held because the user has been in touch
with them by email outside Sliq. When it says Instantly isn't connected or refused the key,
call get_tool_connect_url(integration_id='instantly') and give the user the link it
returns, never a placeholder, so they can connect a working key. A refused key's error ends
with Instantly's reason: when that reason names a scope the key needs, tell the user their
new key needs it. When it says the workspace has no active paid plan, tell the user to
reactivate their Instantly plan; the handoff resumes on its own once they do. cancel_kind
says why a handoff was cancelled or stopped: they
replied (recipient_replied), the user wrote to them (manual_reply), they RSVP'd to an
event (event_rsvp), their address bounced
(email_bounced), they were removed from the campaign (prospect_skipped) or marked with a reply outcome
(manual_mark), moved to another campaign (campaign_reassigned) or off the step
(prospect_moved), the campaign was deleted (task_deleted), the user skipped the card
(user_rejected), or they unsubscribed in Instantly (unsubscribed).
mail_host is the email provider hosting the person's address (Google Workspace,
Microsoft 365, …), read from their domain's MX record when the address was saved:
google, microsoft, gateway (a security gateway such as Mimecast or Proofpoint
receives the mail first, which hides the provider behind it), other (any other host),
unknown (no mail record, or the domain's DNS is broken), or '' when there's no email
or no lookup of it has succeeded yet. To filter or route on who hosts a prospect's mail
("Google-hosted only"), read this column, since a domain name alone doesn't show it.
timezone is the IANA time zone the person is in (such as America/Los_Angeles): one set
by hand, else the one their location names. It is '' when unknown: they have no
location, it names no single zone (a country spanning several), or it isn't classified yet.
Set it with update_prospect's timezone when you know where they are.
linkedin_messages_sent / last_outbound_at are the count and latest timestamp
of outbound LinkedIn messages sent to the prospect (hand-sent ones included; a
connection-request note isn't a message, even though LinkedIn replays it into the
chat when they accept).
For any follow-up check, gate on linkedin_messages_sent, never on
linkedin_stage alone. linkedin_stage = 'messaged' is a flat bucket — it can't
tell a prospect who's had only the first message from one who's already had several,
so selecting on the stage (even with a last_outbound_at cutoff) re-sends a
follow-up that already went out. Gate the exact step: linkedin_stage = 'messaged' AND linkedin_messages_sent = 1 AND last_outbound_at <= '<5 days ago>' is the first
follow-up; = 2 with a 7-day cutoff is the second. Both fields reflect synced
LinkedIn history, which usually syncs within minutes: reliable for day-grained
follow-up checks.
Stage meanings — translate these for the user (say "still being looked up", not
"resolving"). email_stage and linkedin_stage share one vocabulary; a rung marked
"(LinkedIn)" never appears on email. not_started (blank stage, never enrolled — no send
has gone out on any channel) is reported separately in the stats return, not a stored stage.
resolving (LinkedIn) — looking up their profile before anything can send; NOT yet contacted.
warming (LinkedIn) — a warm-up flow is engaging their content (comments, reactions) to build familiarity, before the first outreach or between steps; not a send in itself.
graduated (LinkedIn) — the warm-up finished; what it triggers next (a connection request, an intro message, or nothing) depends on the flow.
pending — scheduled to go out (show the scheduled time if present).
sent — the connection request (LinkedIn) or the email has been delivered.
connected (LinkedIn) — they accepted the connection request.
messaged (LinkedIn) — a follow-up message went out after they connected.
replied — they replied (a neutral/unclassified reply). Either channel.
interested — they replied with clear interest (a positive reply). Ranks above replied. Either channel.
meeting_booked — they agreed to or booked a meeting in their reply. The top rung. Either channel.
not_interested — they replied with an explicit decline. Ranks above
replied, below the positives — still a replier: include it in any "everyone who replied" count. Either channel.unreachable (LinkedIn) — a permanent failure: invalid/locked/not-found profile, or they don't accept invites.
already_connected (LinkedIn) — already a 1st-degree connection, so the request was a no-op; not pursued by default, but you can message them directly.
withdrawn (LinkedIn) — the connection request sat unaccepted past the withdrawal window and was auto-retracted.
Acceptance (LinkedIn connection-request accept rate) does NOT come from the connected
count — accepters advance to messaged/replied, so connected undercounts. Read it from
get_agent_details (tracking_summary.funnel carries per-channel funnels with the connection
acceptance_rate), also surfaced on the per-agent LinkedIn CR acceptance line in your Active Agents context; it matures over ~a week
(an accept lands up to ~8h after the click, requests sit sent for days), so a campaign's first week
reads low. Who/when — who accepted or replied and when, a week-over-week trend — comes from
list_prospect_events, not stats: stats are current positions, the event feed is the history.
Analytics — mode="stats" with group_by a JSONB data key (the per-enrollment
data->>'...', not the canonical top-level identity columns) surfaces patterns:
group_by="title" (which job titles reply/engage), "company", "source" (which lead sources
perform). Each group's per-channel stage maps count every prospect, removed people included.
Everyone who replied is the sum of every reply rung — replied, interested,
meeting_booked, not_interested (the classified rungs rank above a neutral reply) — the
replied count alone undercounts. Report per-channel — don't mix the email and LinkedIn maps.
group_by groups carry no acceptance rate. When the split asked for is a person trait such as
title and one of the user's segment groups (list_segment_groups) tags it, read acceptance per
tag from get_segment_rates or get_segment_funnel. Otherwise give the per-agent rate above,
and for a person trait offer to set up a segment group along it. Surface a pattern
proactively when one emerges (e.g. "seed-stage companies reply at ~3× the Series-B rate"),
not only when the user asks.
Dedup before acting — to check whether you've already tracked or acted on someone, query by
whichever identifier you have (omit agent_id to look across all your agents):
where_clause="email='...' OR linkedin_slug='...' OR linkedin_provider_id='...'".
In search mode, {count, truncated, items array}.
In stats mode, {total, by_email_stage: {...}, by_linkedin_stage: {...},
not_started, removed, groups?: {: {total, by_email_stage, by_linkedin_stage}}}.
The top-level stage maps and not_started cover the people still in the
campaign, and removed counts the people removed from it, so total is
the two together. The groups count every prospect, removed people included.
not_started is the count of those with neither channel stage populated
(never enrolled) — in neither stage map. For the campaign's reply counts
and rates with removed repliers included, read tracking_summary.funnel
from get_agent_details.
Inputs: mode, limit, offset, agent_id, +5 more.
query_search_resultsQuery search resultsARead-onlyInspect
Columns: id, agent_id, entity_type, list_name, source, identifier,
display_name, verdict, verdict_reason, data (JSONB), created_at, person_id,
company_profile_id, linkedin_slug, linkedin_provider_id, email. Filter by list
with "list_name = 'oil-gas'". identifier is the key the row was saved under —
a company's domain; for a person, whichever LinkedIn handle or email its source
had. A person's LinkedIn profile slug, provider id and email are in
linkedin_slug, linkedin_provider_id and email, read from the linked person
('' when the row isn't linked or the person has none). source is the
discovery tool that surfaced the row ('agent' when no discovery tool did;
'legacy' for rows older than the column). verdict is the curation call
recorded on the row with record_search_results, or by the check a search
tool, a post monitor's engager capture or a Websets list runs on the rows it
saves — 'qualified', 'rejected', 'unclear' for a row
screened but too thin to judge, or NULL for a row not yet screened — with
verdict_reason saying why. Rejected rows are left out unless you pass
verdict='rejected', include_rejected or run_id, the same as the user's default
view, so a where_clause on verdict alone never reaches them.
person_id / company_profile_id are the canonical person / company FK ids
(NULL until the row is linked). A person's profile (title, company) and a
company's firmographics (industry, location, employee count, description) — the
values the agent's People and Companies tabs show — live on the linked record, not
in the row's own data: read them with query_people / query_companies on
those ids, and filter on them through the id, e.g. "company_profile_id IN (SELECT
id FROM research_app_company_profiles WHERE industry ILIKE '%oil%')".
For a person's LinkedIn degree and warm-intro connectors, or the agent's people
counted by list, source, degree or connector, use list_people instead. For
the agent's companies as its Companies tab shows them, one row per company with its
lists, verdict and signals rolled up across rows, use list_companies.
data has no single schema — its shape follows whichever source wrote the
row (it is the agent-authored blob). Most person rows nest under person
(data->'person'->>'position',
data->'person'->'company'->>'name') and most company rows under company
(data->'company'->>'name'), but other sources
write those fields flat. A path that doesn't exist yields NULL rather than
an error, and a NULL comparison drops the row — so a filter aimed at the
wrong shape comes back empty or short and reads as a genuine miss. Read a
page of rows without a data filter first, then filter against the shape
you see.
Person rows are annotated with the team connection overlay (mirrors the Find
UI): a matched row's data gains connection_status: {you: bool, teammates: [{email, name, first_name}]} naming the viewer/teammates who are 1st-degree
to that prospect; unmatched rows and company rows carry no such key. A
top-level connections_coverage: {you: 'none'|'partial'|'csv_uploaded', team_missing: int} reports the owner's own connection coverage and how many
active teammates still lack a CSV upload. The viewer is the agent owner.
In row mode (group_by omitted) — a dict with count, truncated, and items
array (each row {id, agent_id, entity_type, list_name, source, identifier,
display_name, verdict, verdict_reason, data, person_id, company_profile_id,
linkedin_slug, linkedin_provider_id, email, columns, created_at}),
plus connections_coverage {you, team_missing}. columns is the normalized
[label, value] projection of the row's agent-researched data.columns.
In aggregate mode (group_by set) — a dict {group_by, groups} where groups
is a list of {key, count, qualified, rejected, unclear, unscreened} ordered by
count descending. count is every row the call counts, and the other four split
it by verdict (unscreened: no verdict yet); rejected rows are ones the screen
ruled out, not matches. With group_by "list",
each group also carries criteria: the search criteria a Websets list screens
its rows against, '' for any other list.
Inputs: agent_id (required), limit, offset, run_id, +6 more.
query_search_runsQuery search runsARead-onlyInspect
To see the people or companies one run found, pass its id to list_people or
list_companies as a {"column_key": "run", "operator": "is_any_of", "values": [run ids
as strings]} filter, for the people or companies it found still on the agent, or to
query_search_results as run_id, for the rows of the people or companies it found on the list
it saved to (none for a 'user_added' run, which saves to no list). All of them include the
rejected ones.
A dict with runs and total, the number of matching runs; page with offset += limit while
offset + limit < total. Each run carries id; source, the discovery tool as the
source column of query_search_results names it, or 'user_added' for people the user
added; entity_type, 'person' or 'company', what it looks for, set even when it found
no one; arguments, what the run was called with (for a Websets run, its list's search
settings) and, unless the run's source is 'user_added', list_name, the list it saved
to, even when the call named none; started_at; finished_at, null while a Websets search
is still running; found, the
people or companies it returned, ones the agent already had included; matched and
filtered_out, those whose row in the list the run saved to is qualified or rejected now
(both 0 for people the user added, who get no list row); and new,
those this run brought to the agent: no earlier run returned them and they weren't on
the agent before it started.
Inputs: agent_id (required), limit, offset, person_id, +3 more.
query_segment_peopleQuery segment peopleARead-onlyInspect
Inputs: group_id (required), limit, offset, tag_id, +6 more.
query_suggested_dealsQuery suggested dealsARead-onlyInspect
Inputs: limit, offset, removed, on_behalf_of.
query_tagged_messagesQuery tagged messagesARead-onlyInspect
Inputs: group_id (required), limit, offset, origin, +8 more.
queue_email_outreachQueue email outreachADestructiveInspect
For single personal emails, use draft_email + send_email.
A campaign already sending without a flow keeps calling this tool without
node_id. Every other campaign sends through its flow, even a single email:
create it with define_sequence and add the prospects with
track_prospects, and the flow's send steps call this tool with their
node_id. A call without node_id on an agent that has no flow and hasn't
sent any outreach yet is refused.
An agent is required — create one via create_agent if none exists. This tool creates tracking items for the recipients it queues, so don't also call track_prospects for them. Only call this AFTER the user has explicitly confirmed the outreach.
Follow-ups thread on their own. A recipient this agent has already emailed
gets a reply in that email's thread, sent from the mailbox that sent it
(whatever mailbox says), so one call can mix first emails and follow-ups
across mailboxes; write their body as a reply. Everyone else gets a new email
from mailbox. If the mailbox that last emailed someone is no longer
connected, they get a new email too.
Dict with queued_count, follow_up_recipients (the queued recipients
sent as replies in their existing thread), skipped items, a preview of
the first queued email,
first_send_at / last_send_at (see below), and tracking_skipped —
recipients already tracked in another campaign, so
nothing queued for them. Each entry names existing_agent_title /
existing_agent_id; surface these and let the user place them.
This tool QUEUES; it never sends. Nothing has been delivered when it
returns — a background beat drains each mailbox in derived order at a
human-like pace, minutes to days later. first_send_at / last_send_at
are ISO timestamps bounding the projected send window (from the read-time
forecast), and awaiting_approval counts rows held for the user's approval.
Gated rows are excluded from the window; when every row is gated both bounds
are null and nothing is projected until the user approves.
So report queued work as queued, with the window: "Queued 4, first lands 12:02pm, last 12:11pm." Do NOT tell the user these were sent.
Writing a marker into a campaign's own tracker (a sheet, a CRM field)
at queue time is fine and is usually what its dedup depends on — skip
it and the next run re-queues the same people. But that marker records
handoff, not delivery: don't cite it back as proof a send happened, and
don't let it turn into "sent" in your summary. For what actually went
out, read the queue (manage_email_outreach_queue(action='status'));
to act at real send time, the agent needs a sent_email trigger.
Inputs: agent_id (required), recipients (required), enrich, mailbox, +4 more.
queue_giftQueue giftAInspect
Use it when the user asks to send someone a gift, and on a campaign flow's gift step,
where the card counts as that step for the prospect and the step's product, price
ceiling and note fill whatever you leave out. Queued cards show in
get_pending_approvals under the gift bucket.
A card the user sent as a link has the status awaiting_claim until the recipient enters
their address, which puts the card back on the user's approval list with that address; the
user's approval then orders the gift. When the user asks for that link, or to put it in a
message, call query_prospects for the prospect and pass on the claim_url of their gift
in gifts as is, never a placeholder.
An approved gift is charged to the payment card the agent's owner saved for gift purchases
under Settings → Billing; with none saved, approving orders nothing and links there to add one.
That payment card is charged the order's actual total, at most max_price_usd, plus about
$3.30 + 2.9% in fees on top.
A dict with success true, gift_id (the card's gift id) and status (the card's
status). On a flow step, nothing is queued and success is false with an error
sentence when the prospect already has a gift in progress for the step (the error
names it) or has since moved off the step (reason is then 'stale_enactment').
Inputs: agent_id (required), prospect_id (required), gift_note, product_url, +3 more.
queue_instantly_handoffsQueue instantly handoffsAInspect
A prospect no longer waiting on the step, already handed off on it, or removed from the
campaign is skipped. A prospect whose address bounced or was unsubscribed from the user's
emails, or who has no stored address, is blocked on the step instead.
A dict with queued, one entry per handed-off prospect with its prospect_id and
status ('pending_approval' while it waits on the user, else 'pending'), and skipped
and blocked, one entry per prospect left out with its prospect_id and reason.
Inputs: node_id (required), agent_id (required), prospect_ids (required), on_behalf_of.
queue_linkedin_outreachQueue LinkedIn outreachADestructiveInspect
A campaign already sending without a flow keeps calling this tool without
node_id, as do a one-off send outside a campaign and a resolve look-up.
Every other campaign sends through its flow, even a single message or
connection requests with nothing after: create it with define_sequence
and add the prospects with track_prospects, and the flow's steps call this
tool with their node_id. A call without node_id on an agent that has no
flow and hasn't sent any outreach yet is refused.
Only call this AFTER the user has explicitly confirmed the outreach.
BATCH: pass ALL recipients in a single call. The tool is designed to
batch — provider_id recipients queue directly (resolving before send
unless already verified) and URL/slug recipients route through the
background resolving queue with rate-limited, business-hours pacing.
Calling once per recipient multiplies prompt overhead by ~N and produces
no observable benefit. If you have 27 prospects to queue, that's ONE call
with recipients of length 27, not 27 calls of length 1.
Dict with known_queued (provider_id recipients queued directly),
deferred_count (URL/slug recipients queued for background resolution),
awaiting_approval (how many of those the user's approval settings hold
until the user approves them in Sliq — tell the user these wait on their
approval; only the rest are on their way. A row whose profile is still
being looked up counts by those settings and reaches the approval queue
once its lookup finishes; the lookup can also hold a row the count left
out, when it shows the user already talks with that person on LinkedIn),
tracking_skipped (recipients already tracked in another campaign, so
nothing queued for them — each entry names existing_agent_title /
existing_agent_id; surface these and let the user place them), skipped
(recipients the queue helper refused — each entry has target_name,
provider_id, reason, error, plus existing_agent_id /
existing_agent_title when a row on another campaign is what
blocked the send; name that campaign to the user rather than calling it
a duplicate on this one), accept_followup (present only when connection
requests queued to this agent but no sequence node or accept trigger
will fire a message on acceptance — names how to wire one), and
rate_limit_estimate — an ETA for the batch covering expected profile
resolution and sending days given the user's current rate-limit budget.
Its summary names the rows awaiting approval and gives the ETA for the
rest only.
Inputs: recipients (required), action_type (required), enrich, notify, +4 more.
queue_linkedin_post_engagementQueue LinkedIn post engagementADestructiveInspect
Exactly one of comment_text or reaction_type must be provided.
For a comment, pass comment_text (the text to post). For a reaction,
pass reaction_type (one of 'like', 'celebrate', 'support', 'love',
'insightful', 'funny'). The agent picks reaction_type='like' by
default for most posts; the user can change it on the approval card.
To engage with a comment on a post rather than the post itself, pass a
top-level comment's comment_id from query_linkedin_post_engagements as
parent_comment_id in place of post_url: comment_text then posts a
reply threaded under that comment, and reaction_type reacts to it. Pass
exactly one of post_url or parent_comment_id. A reply opens with a tag
of the commenter, as LinkedIn's Reply button adds one, so comment_text is
what follows the tag: start it with the reply itself rather than their name.
With tag_commenter=False it posts untagged. Before replying, read the
comment's replies with query_linkedin_post_engagements
(parent_comment_id = '<its comment_id>'): on the user's own post, a reply
with is_post_author true means the user already answered, and a reply_count
above the replies stored means some replies weren't captured.
Resolves the post's social_id (URN format) from the URL, or takes it from the
stored comment's post when parent_comment_id is passed, then creates
a linkedin_invite_queue item with action_type='comment' or 'reaction'
depending on which body field was set. The pre_save signal resolves the
per-subtype approval policy — gate (pending_approval) or auto-send (pending).
When you aren't working on an agent (over MCP, in a chat outside any agent, or with
on_behalf_of), it queues under that account's LinkedIn Quick Actions agent, which holds
one-off LinkedIn sends.
A queued-status dict in one of three forms.
{'queued': True, 'awaiting_approval': bool, 'message': str} on success;
awaiting_approval is False when the engagement will post automatically
{'queued': False, 'deduped': True, 'message': str} when this post (or the
comment) already has a live engagement of the same kind (report the
message; not an error)
{'queued': False, 'error': str} when it can't be queued, e.g. the post doesn't
resolve or the comment is too long; the error says why
Inputs: post_url, post_text, author_name, comment_text, +5 more.
read_attachmentRead attachmentARead-onlyInspect
Read a slice of any file in the user's attachment library, by id.
Use this when the attachment summary doesn't contain enough data to answer, or to see an image: an image comes back whole, so the slice arguments don't apply. For spreadsheets, paginate with row_start/row_end (default window = 500 rows). For PDFs/PPTX and text files, use page_start/page_end (1-indexed, inclusive; default window = 10 pages). When the result has truncated=True, advance page_start to read on.
Inputs: attachment_id (required), sheet, columns, row_end, +4 more.
read_email_attachmentRead email attachmentARead-onlyInspect
Use this to read the contents of an attached document or image (e.g. "analyze this
spreadsheet", "summarize this PDF"). Parseable types (.csv, .tsv, .xlsx, .pdf,
.pptx, .txt, .md) and images come back readable — call read_attachment(attachment_id)
to pull a document's row/page slices or see the image.
Any other type is saved to the user's library but isn't readable; use
get_email_attachment instead if you only need to fetch or transfer the file.
Provide email_data_id + attachment_id (from search_emails results). Dict with attachment_id (pass to read_attachment), filename, mimeType, summary.
Inputs: on_behalf_of, attachment_id, email_data_id.
read_linkedin_attachmentRead LinkedIn attachmentARead-onlyInspect
Use this to read a file someone sent or received on LinkedIn (e.g. "summarize the
deck they sent me"). A document, spreadsheet, slide deck or image comes back readable:
call read_attachment(attachment_id) to pull a document's slices or see the image. Any
other type, such as a voice message, is saved to the library but isn't readable.
Dict with attachment_id (pass to read_attachment), filename, mimeType, summary.
Inputs: message_id (required), attachment_id (required), on_behalf_of.
record_search_resultsRecord search resultsAInspect
The discovery tools (exa_find_people, search_linkedin_people,
fetch_post_engagers, theirstack_search, find_companies_by_tech_stack,
exa_search_news, search_news_events) already save and link their own matches
when run in an agent. Use this tool on their rows to filter them (a verdict
on each screened row, below) or to add to a saved row — columns, signals,
or evaluations — always under the same identifier and list_name the
discovery tool saved it with; a different list_name makes a second row.
Dedup per (agent, identifier, list_name). On an existing row, a field you
leave out keeps its stored value. In columns a blank you send is written
as sent; anywhere else, including inside person, a blank ('' or null)
keeps the stored value too. data['signals'] accumulates across runs
(union by URL), and any other value you send replaces the stored one.
Cross-list duplicates of the same identifier are allowed (intentional — two
lists may track the same entity for different reasons).
Identifiers are stored canonical — the registrable domain (eTLD+1) for
companies (https://www.Acme.com/ and news.acme.com both → acme.com),
bare slug for LinkedIn (https://linkedin.com/in/JohnDoe/ → johndoe).
Match the canonical form when filtering identifier via query_search_results;
a person row whose linkedin_slug is set is filterable by that slug, whatever its source.
Empty-list semantics: signals: [] preserves accumulated signals (the
cross-run contract); evaluations: [] REPLACES (LLM-rewritten per run).
Put every user-requested display attribute — a metric, a person's email,
any column the user asked to see — in data.columns as {label: value},
one entry per column under the label the user asked for. columns merges
by key-union across runs (new values win). The Output tab and CSV render
these as columns; a value written to a top-level extra key instead is not
surfaced.
Set each result's source to the discovery tool that surfaced that
candidate — per row, so a batch merged from several tools keeps each row's
origin. Leave it at 'agent' only for rows no discovery tool surfaced.
A Websets list screens its own new rows against the search's criteria, and
a discovery tool checks the rows it saves against the criteria it was
called with, each giving a row a verdict, 'unclear' when its details are too
thin to judge, so a verdict you record on those rows overrules that one. For
any other row, this is also
how you filter what you screened: give each screened row a verdict —
'qualified' for one you keep, 'rejected' with a verdict_reason for one you
drop — under the identifier and list_name it was saved with, so the verdict
lands on that row.
To mark a row already saved, re-record it with its identifier, display_name
and list_name plus the verdict; the data you leave out stays as stored. A
rejected row stays saved but drops out of the list the user sees by default;
the table counts it as "didn't match" beside its own count, and the user
clicks that count to see those rows with their reasons. query_search_results,
list_people and list_companies leave it out the same way
unless you ask for rejected rows. A row recorded without a verdict keeps whatever verdict it already has.
{created, updated, total, created_identifiers} — created_identifiers are
the canonical identifiers of the rows this call added (not ones it merged into).
Inputs: results (required), agent_id (required), list_name (required), entity_type, +1 more.
register_manual_linkedin_invitesRegister manual LinkedIn invitesAInspect
Only registers invites still pending; anything else comes back as not_pending — which can
mean already accepted or never sent, so don't assume acceptance (check the prospect's
linkedin_stage before messaging). Unmatched prospects that have a LinkedIn URL but no
provider_id yet come back under resolving: Sliq looks up their profile in the background
to identify them (in progress, not failed). Attach a resolved_profile trigger to act on
each result — an already-connected one sends no acceptance event, so it won't engage on its
own. Use this only after the prospects are tracked and the user has confirmed they sent the
requests themselves.
Usually returns in seconds: the pending-invitations read stops as soon as every named prospect is matched, and recently hand-sent invites sit at the top of the list. When some prospects aren't found there, it pages the user's full backlog with paced gaps to protect their LinkedIn account — up to a couple of minutes on a large backlog — so warn the user about the possible wait only when registering invites sent long ago or likely already accepted, not for a just-sent batch.
Pass the specific people the user named in identifiers. Only set all_tracked=True
when the user said they hand-invited the whole campaign — it registers every tracked
prospect that has a real pending invitation, which can pull in an unrelated pending
invite, so it's a deliberate opt-in rather than the default.
Dict with registered / already_registered / not_pending / resolving / ambiguous /
skipped lists, agent_has_accept_trigger (whether the agent has an accept trigger — a
sequence campaign advances on accept via its flow regardless), and a summary message.
Inputs: agent_id (required), node_id, all_tracked, identifiers, +1 more.
remove_triggerRemove triggerAInspect
Removing a flow node's on_enter enactment un-arms that node — prospects entering it
no longer enact until you re-arm it with add_trigger. To stop it firing while keeping
its authored content, pause it with update_trigger(status='paused') instead.
Dict with success, agent_id, and next_run_at. Read the agent's surviving
triggers via get_agent_details.
Inputs: trigger_id (required), on_behalf_of.
report_enactment_blockedReport enactment blockedAInspect
Inputs: reason (required), node_id (required), agent_id (required), prospect_id (required), +1 more.
request_user_actionRequest user actionAInspect
Prefer on-platform tools when the channel is automatable: queue_linkedin_outreach
for LinkedIn DMs and connection requests, the email tools (draft_email,
queue_email_outreach) for email outreach, find_email for contact lookups.
Use this tool only after ruling those out.
This tool is for a one-off ask about a particular prospect. When the off-platform
step is a step of a sequenced campaign — every prospect reaching that point gets it,
and the campaign continues once it's done — author a manual node in
define_sequence instead: entry queues the same row, and marking it done advances
the prospect onto the node's next step.
Dedup is per pending row on (agent, prospect, bucket_name): if the same
prospect already has a pending row for the same bucket the tool returns
created=False with the existing row's bucket and action_description so you
can see what's already queued. Re-flagging the same prospect for a different
bucket is allowed; re-flagging after the user marks a prior row done or
dismisses it is also allowed.
Dict with created (bool), action_id (int or None), id (int), and reason (str).
Inputs: agent_id (required), bucket_name (required), prospect_id (required), action_description (required), +2 more.
resolve_dateResolve dateARead-onlyInspect
Also use this to convert times in other timezones to the user's local timezone. For example, if someone says "2pm PST" and the user is in Eastern, call resolve_date("2pm PST") and it will return the equivalent time in the user's timezone. Use standard timezone abbreviations (PST, PDT, EST, EDT, CST, CDT, MST, MDT, GMT, UTC, etc.) -- "Pacific" or "Eastern" alone won't parse.
IMPORTANT: When using the result for trigger_at in add_trigger, ALWAYS
use the utc_iso field from the response (e.g. "2026-04-04T04:56:00Z").
The time and time_12h fields are in the user's LOCAL timezone — passing
them with a "Z" suffix will create triggers at the wrong time.
Dict with date, weekday, and (when time is present) time, time_12h,
and utc_iso (UTC ISO format for use in triggers).
Inputs: expression (required), relative_to, on_behalf_of.
restore_page_versionRestore page versionAInspect
Inputs: page_id (required), version (required), on_behalf_of.
retry_blocked_enactmentRetry blocked enactmentAInspect
A retry with the cause still in place fails and re-blocks identically — fix the
cause in the same call: pass email for an email-shaped block (email_not_found,
a missing address) and/or linkedin_slug for a wrong or missing LinkedIn
identifier (missing_identifier, a failed resolution). Both fix the EXISTING
prospect's person — never re-add the person via track_prospects with the new contact
info, which creates a duplicate prospect. On an Instantly step, missing_identifier
means the prospect has no stored email address, so pass email for it. A
cr_already_sent block has no fix here: Sliq's records show an earlier connection
request to this person that is queued, was sent, or failed, and Sliq won't queue a
second one while that request stands. Tell the user; if they have since invited them
by hand, call register_manual_linkedin_invites, which picks the campaign up from that
invite; otherwise drop the prospect with update_prospect(stage='skipped'). A
name_mismatch block means the LinkedIn profile belongs to someone other than the
person they were added as (data.supplied_name):
retrying continues the campaign with that profile — do it only when it's the same
person under another name; otherwise skip the prospect and track the intended person
(a linkedin_slug fix is refused on this block). An instantly_failed block means the
prospect's Instantly handoff failed; the last_error of the last entry in their
instantly_handoffs on query_prospects says why. When it says Instantly refused the lead
and Instantly's message is about the address, retry with a corrected email. When the
lead was moved out of the campaign or is gone from the workspace, someone changed it in
Instantly: ask the user whether to hand the prospect off again, then retry, or drop them
with update_prospect(stage='skipped'). For any other cause, tell the user what it says
and retry once they have fixed it. An
instantly_already_in_campaign block means the address was already a lead in that
Instantly campaign, which Sliq left as it was: once the user removes that lead in
Instantly, retry. An email_unsubscribed block has no fix here: the person unsubscribed
from the user's emails in Instantly. Tell the user. If they want the campaign to go on
without email, move the prospect past the step with move_prospect_to_node; to stop,
drop them with update_prospect(stage='skipped').
Dict confirming the prospect, the node they rest on, the cleared reason,
and any contact fixes applied (email / linkedin_slug). An email fix lands on
the person's profile, the prospect's only address.
Inputs: agent_id (required), prospect_id (required), email, on_behalf_of, +1 more.
save_memorySave memoryAInspect
Organize the document into these sections (create as needed):
## About the User and Their Company— role, background, what they're building, who they sell to, key customers, value prop## Scheduling Preferences— default meeting duration, preferred times, buffer between meetings, preferred days, meeting-free blocks## Communication Style— email tone, sign-off preferences, formality level, writing patterns## Working Patterns— focus time blocks, availability windows, time zone, work hours## Key Relationships— important contacts, their roles, how they prefer to communicate## Other Preferences— anything that doesn't fit above
When updating an existing preference (e.g. correction to default meeting duration), edit the existing line rather than duplicating. Dict with success status
Inputs: content (required), on_behalf_of.
save_prospect_researchSave prospect researchAInspect
Call this after you synthesize a "why I'm reaching out" angle for a recipient — once per recipient, batching all of that person's findings (and you may batch across recipients). Each finding is YOUR synthesized insight ("former Stripe PM, just posted about scaling support"), NOT raw scraped post text or an article abstract.
Pass the lead in each finding's person. A company finding (subject_kind='company') still
attaches to the person it personalizes; set company_domain to record the
company.
Dict with created (the count of saved research rows).
Inputs: agent_id (required), findings (required), on_behalf_of.
search_calendarSearch calendarBRead-onlyInspect
Inputs: where_clause (required), limit, offset, order_by, +1 more.
search_chat_historySearch chat historyARead-onlyInspect
Inputs: limit, keyword, on_behalf_of, created_after, +1 more.
search_emailsSearch emailsARead-onlyInspect
Inputs: where_clause (required), limit, offset, order_by, +1 more.
search_linkedin_connectionsSearch LinkedIn connectionsARead-onlyInspect
position and company are connection-time snapshots and may be stale — treat them as a starting point, not current ground truth. Dict with count, truncated (True when more rows exist past this page), and connections array.
Inputs: where_clause (required), limit, offset, order_by, +1 more.
search_linkedin_message_historySearch LinkedIn message historyARead-onlyInspect
Inputs: where_clause (required), limit, offset, order_by, +1 more.
search_linkedin_peopleSearch LinkedIn peopleARead-onlyInspect
One call runs one query, returning ~10 matches by default (one page). To go
deeper on a single query — "find people in my network matching my ICP" — pass
max_results (up to 100): the tool pages through the matches for you, each
~10-profile page counting as one search against the daily budget. For someone
whose profile URL or provider_id you already have, read their degree from
list_people or a resolve look-up and pass the identifier straight
to queue_linkedin_outreach to queue outreach; a name search would only add
namesakes. To search different people — a list of names, or one filter per
company — loop this tool inside a run_code block, one call per name or
company (that's breadth; max_results is depth on one query). Searches are
paced about 6 seconds apart (per search, and per ~10-profile page of a deep
one) and serialized across this user's LinkedIn work, so a 20-name loop runs
about 2 minutes and a 90-name loop about 9. The user sees only a running step
until the loop returns, so when a loop will run more than about 20 searches,
write them a one-line note with the count and the wait ("Looking up 40
profiles now, this takes about 4 minutes.") as text in the same response,
before the run_code call. If a search comes back paused or rate-limited, stop and tell the
user which searches remain — the account is paused and further calls won't run
until it lifts.
Scope filters combine with keywords and can be used alone for a single
filtered search:
connections_of— restrict to the first-degree connections of specific people, passed as theirprovider_ids (as returned by an earlier search or profile lookup). To work up to a buyer through someone the user just connected with, pass that person inconnections_ofand the target company inadvanced_keywords={'company': 'Acme Corp'}to surface who they know there.network_distanceis a separate filter on the user's own degree and combines with this — add [2] to keep just the connections the user isn't already directly linked to.advanced_keywords— native LinkedIn keyword sub-filters: a dict with any offirst_name,last_name,title,company,school(each a string).companymatches loosely, so its results can include people who don't work there now (a past employer, a similarly named organization).profile_language— ISO 639-1 codes (e.g. ['en']) that narrow any of the above to profiles written in those languages. A refinement, not a search on its own — pair it with keywords or another filter.
Each profile the search returns is then checked against criteria, one
profile at a time, and only the matches come back in profiles; the rest
come back in rejected as {name, reason}, with no profile URL, so they can't
be acted on. The check reads the name, headline and location, so a criterion
a headline can't show (company size, industry) doesn't reject anyone. It adds
a few seconds per ten profiles.
When this runs in an agent, the profiles are saved with their verdicts and
linked to the workspace Output tab automatically (deduped by profile), the
rejected ones hidden from the list's default view. Pass list_name (a short slug) to name their list — a
discovery search, the people connected to someone, prospects to work through;
reuse the same slug across a loop or follow-up searches to gather everything
into one list. Absent a slug, they land in the 'default' list. Outside an
agent, results are returned only.
Returns up to max_results profiles with provider_id, name, headline,
network_distance, location, and profile_url. Each match's headline is the
member's own tagline: often their current role and company (e.g. to see which
companies 2nd-degree matches work at), but frequently a title alone, so a
headline that omits a company is not evidence they work elsewhere. total_count is LinkedIn's full match count for the query when it
returns one, but LinkedIn now omits it on most Classic searches (so it's often
null): only say "showing N of ~M" when it's a number exceeding the profiles
returned, and never invent a total. Use has_more — True when more results
exist beyond those returned — to decide whether to offer to pull more.
Present the results to the user so they can pick the right person. An error
about being "heavily queued" is transient pacing back-pressure — retry shortly
rather than reporting it as not found.
That field list is the whole of it — a search result carries no current
employer, connection count, follower count, or employment history. Present
what comes back as it is; a search the user wanted to look at is finished at
that point. When the ask genuinely needs one of the missing fields — a
connection-count threshold, employment history to personalize from, or a
per-company verdict or count from company-filtered searches ("which of
these accounts do I have a path into") — pass the matches' profile URLs to
enrich_linkedin_profiles, which returns them for the whole list in one
paid call (connections_count is the field a connection-count filter reads;
company is the current employer) and spends no LinkedIn account budget. A
per-company verdict needs it because an answer summed over people hides the
headlines that would show a loose match. When the user will read a
company-filtered list themselves, present it and offer that enrichment to
confirm who works there now. When the decision also turns on
whether the user is already connected to them, use
queue_linkedin_outreach(action_type='resolve') instead; connection status
is the one thing enrichment cannot answer.
Rate-limited — shares one daily LinkedIn search budget with all other
LinkedIn people searches.
On success, a dict
{'success': True, 'profiles': [...], 'rejected': [...], 'total_count': int | None, 'has_more': bool, 'searches_remaining_today': int}. profiles holds the
matches among the up to max_results found, each with a verdict
('qualified', or 'unclear' when the profile says too little to judge) and
its verdict_reason, and a null verdict when the check couldn't run;
rejected holds the rest as {name, reason}. total_count is the query's full match count when
LinkedIn returns one (often null since its Aug-2026 Classic Search change),
so lean on has_more for whether more results exist; and
searches_remaining_today is the post-search budget, so you can size a
follow-up loop without re-checking. In an agent, also saved_to_list (the
list the profiles were saved to) and saved_count, how many of them the list shows; outside an agent, a
passed list_name yields a null saved_to_list with a persist_note. If
the account tripped its pause partway
through paging, the (still valid) partial results come back with
paused: True and a note — surface it: further searches won't run until
the pause lifts.
On a failed search: {'success': False, 'profiles': [], 'rejected': [], 'error': ..., 'searches_remaining_today': int}. On a pre-flight refusal (daily limit
reached or account paused), searches_remaining_today is omitted:
{'success': False, 'error': ...}.
Inputs: criteria (required), keywords, list_name, max_results, +5 more.
search_meetingsSearch meetingsARead-onlyInspect
When the user has Grain, Granola, or Otter connected, prefer this tool first (Fathom data is local and faster) and fall back to grain_*/granola_*/otter_* only if no relevant results or the user explicitly references that source.
Dict with count, truncated, and meetings array (summary truncated to 6000 chars, transcript truncated to 10000 chars — use get_meeting_transcript(id) to fetch the full transcript for a specific meeting)
Inputs: where_clause (required), limit, offset, order_by, +1 more.
search_monitor_eventsSearch monitor eventsARead-onlyInspect
Search the user's monitor event log. Use for scheduled digests (only_unnotified=True) and ad-hoc questions ('what's the latest on Stripe?').
Returns a dict with total (full match count), truncated (True when more
rows exist past this page — page with offset to reach them), and events.
Inputs: limit, since, offset, agent_id, +4 more.
search_news_eventsSearch news eventsAInspect
⚠️ DISCOVERY ONLY — NOT a company lookup. This returns a global feed of the
most recent events matching categories + company_locations (deduped by
company domain, ~10 rows). It takes NO company / domain / name input and
CANNOT be scoped to a specific company or to a list of known companies.
Use it ONLY for find-companies discovery ("surface net-new companies that
just did X"). To research signals for a SPECIFIC named company or an
uploaded prospect/account list, this is the WRONG tool — use
tavily_search with a company-scoped query (e.g. " customer
success news 2026") instead. search_monitor_events(company_domain=...)
works too, but only for companies the user already monitors.
Use this when the user asks to DISCOVER companies by a signal that maps to one of the supported categories below. Only fall back to tavily_search if this returns no results or the signal isn't covered.
Costs 0.3 Sliq credits per event record PredictLeads returns, up to 10 records per location (so up to 3 credits per location). A location query that fails is free.
Each event's company is then checked against criteria, one company at a
time, reading its name and the event, and only the events at matching
companies come back in events; the rest come back in rejected as
{name, reason}. It adds a few seconds per ten events.
When this runs in an agent, every event company with a domain is saved to
the Output tab as a company row (deduped by domain) with its verdict,
carrying the event in data.signals; the rejected ones are hidden from
the list's default view.
Supported categories (pass one or more in a single call):
Expansion: expands_facilities, expands_offices_in, expands_offices_to,
opens_new_location, increases_headcount_by, attends_event
Investment: receives_financing, goes_public, invests_into,
invests_into_assets, has_earnings, has_revenue, has_valuation
Leadership: hires, leaves, promotes, retires_from
Contract: signs_new_client, loses_client
Cost cutting: decreases_headcount_by, closes_offices_in
New offering: launches, is_developing, integrates_with
Partnership: partners_with, ends_partnership_with
Acquisition: acquires, merges_with, sells_assets_to
Challenges: declares_bankruptcy, files_suit_against, has_issues_with
Recognition: receives_award, recognized_as
Relational: identified_as_competitor_of, spins_off_company, spins_off_division
Dict with events, the events at matching companies, each with its
company's verdict ('qualified', or 'unclear' when the event says too
little to judge) and verdict_reason, and a null verdict when the
check couldn't run; rejected, the companies the check turned down,
as {name, reason}; count, every event found, rejected ones included;
and created_identifiers, the domains of matching companies this call
newly added to the list. If every location-query failed with an
upstream error (timeout / 5xx) and nothing landed, also
predictleads_available: False plus a human-readable error — a source
outage, NOT a genuinely empty feed. Partial success (some locations
errored, others returned events) stays silent and reports only the
events.
Inputs: criteria (required), categories (required), agent_id, list_name, +3 more.
send_emailSend emailADestructiveInspect
The sending mailbox is resolved automatically from the draft log created by
draft_email or draft_reply — no mailbox arg needed.
Dict with success status. On a Superhuman mailbox it also carries
sent_at, up to a minute ahead of the call — Superhuman holds the
message for its Undo Send window and it leaves the outbox then. If the
user says a just-sent message hasn't arrived, tell them it lands at
sent_at rather than sending it again.
Inputs: draft_id (required), provider, on_behalf_of.
send_luma_invitesSend Luma invitesADestructiveInspect
Luma emails each recipient an invite to the event and adds them to the
roster as invited. This is a real send with no approval step and no
undo — treat calling it as sending, and confirm the recipient list with
the user first unless they have already approved this exact wave.
Distinct from the LinkedIn/email outreach queues: those are Sliq-sent cold sequences with pacing and approval gating, whereas this is Luma's branded event invite from the calendar the user owns. Use the queues to pitch the event to people who don't know it exists; use this to invite people who should land directly on the event's guest list.
Anyone already on the roster — invited, going, pending approval, waitlisted, or declined — is skipped, so re-running with the same list invites only the genuinely new people. That makes retries safe.
Every invite is recorded against the matching person Sliq tracks (under
data['luma_invites'], one entry per event), so who-we-invited is
answerable from our own records and not only from Luma's roster.
Recipients Sliq isn't tracking are still invited — they just get no such
record, which means an RSVP from them won't appear in the funnel. That is normal (hand-typed addresses, people who were
never prospects), and most waves have some, but the user can't see it
from Luma's side. When not_tracked is non-empty, say so in the reply
— how many, and which addresses — instead of reporting a clean send.
A summary dict
{event_id, invited: [...], invited_count, skipped: [{email, reason}],
skipped_count, tracked_count, not_tracked: [...], not_tracked_count}
— plus note, user-facing wording for the untracked recipients,
present only when there are any. invited lists the addresses
actually sent to; a fully-deduplicated call returns invited_count 0
and is a success, not an error. tracked_count counts recipients
Sliq already knows; it is lower than invited_count whenever the wave
includes people who aren't tracked.
Inputs: emails (required), event_id (required), message, on_behalf_of.
set_activity_hoursSet activity hoursAInspect
Inputs: end, start, timezone, active_days, +1 more.
set_agent_modelSet agent modelAInspect
Inputs: model, on_behalf_of, use_own_anthropic_key.
set_daily_digestSet daily digestAInspect
Inputs: on_behalf_of, email_enabled, slack_enabled.
set_linkedin_daily_limitsSet LinkedIn daily limitsAInspect
limits is a partial {action: cap} map — only the actions you name change, the
rest keep their current cap. Each cap is clamped to that action's tier-aware ceiling
(the ceiling in check_linkedin_daily_usage's configurable_limits); a cap of 0 or
below resets that action to its default. Action keys are the configurable_limits
keys (connection_request, message, inmail, …); an unknown key is rejected with the
full valid list.
The number the user sets is the day-to-day center, not a fixed count: like Sliq's defaults, each cap carries a small ±jitter so sending doesn't read as automation, so the actual daily number varies by a few around the set value (never above the tier ceiling). Tell the user the value you set, not that they'll send exactly that many.
Returns {success, custom_daily_limits}, the resulting stored cap map.
Dict with success and the stored custom_daily_limits map.
Inputs: limits (required), on_behalf_of.
set_linkedin_warmupSet LinkedIn warmupAInspect
disabled=True opts out of the ramp (full daily volume right away — only safe on an
already-warm account); disabled=False re-enables it (resumes any active ramp).
Returns {success, warmup_disabled, is_ramping_up}, the recomputed ramp state.
Dict with success, warmup_disabled, and is_ramping_up.
Inputs: disabled (required), on_behalf_of.
set_outreach_approvalSet outreach approvalAInspect
settings is a partial {subtype: requires_approval} map — True holds the
message for the user's approval, False auto-sends. Only the subtypes you name
change. Each subtype key names a message type (e.g. email_first_message); an
unknown key is rejected with the full valid list.
scope:
'this_agent' (default) — change approval for THIS agent only, leaving the account-wide setting and other agents untouched. Needs an agent in context (or an explicit agent_id). This pins the agent to its own approval settings that no longer follow account-wide changes — tell the user that.
'account' — change the account-wide default that governs every agent with no override of its own. An agent that has its own override is unaffected — warn the user that an account change won't reach any agent that has its own override.
'inherit_account' — clear THIS agent's own approval settings so it follows the account-wide setting again.
settingsis ignored.
Returns {success, scope, settings} — settings is the resulting resolved
map for the level written.
Dict with success, scope, and resulting settings.
Inputs: scope, agent_id, settings, on_behalf_of.
set_personalization_guideSet personalization guideAInspect
Inputs: guide (required), on_behalf_of.
set_privacy_settingSet privacy settingADestructiveInspect
Inputs: store_only_prospect_data (required), on_behalf_of.
set_reply_alertsSet reply alertsAInspect
Inputs: reply_alerts (required), on_behalf_of.
set_up_linkedin_monitoringSet up LinkedIn monitoringAInspect
An agent can host several named monitors. mode='network' watches the user's
whole 1st-degree feed; mode='list' watches an explicit set of people and
company pages — pass members as profile URLs or vanity slugs for people, and
the full linkedin.com/company/... URL for a company page (a bare company name
or slug is read as a person's slug); mode='topic'
searches all of LinkedIn for posts matching keywords, beyond the user's
network. Watching the user's own profile (their
URL in members) is how a user gets engager capture on their own posts.
writing_instructions steer the comment voice. An edit to an already-active
list monitor waits for its next scheduled run, because every list run bills each
watched profile; every other call kicks off a run immediately, so a new monitor or
an edit shows results within minutes. After that it runs on run_weekdays (default Mon/Thu, in the user's timezone). The first run covers
only the members present at creation, and edit_monitor_members starts no run — so create a list monitor with its
complete membership in this one call (assemble a large or query-derived list
in run_code and pass the whole variable), since members added afterward miss
the backlog their first scheduled run would have caught. Comments land in the
approval queue by default — they post without review only if the user has
enabled auto-approve for the linkedin_comment subtype.
list mode works even when the user hasn't connected LinkedIn: discovery is
cookieless and drafting never touches their session, so drafted comments hold
in the approval queue and post once they connect. network/topic discover
through the user's own LinkedIn session, so those two need it connected first.
Schedule and recency are per-monitor. run_weekdays is which days the
monitor runs — weekday ints Mon=0 … Sun=6 in the user's local timezone (e.g.
[3] = Thursdays only, [0,1,2,3,4] = every weekday). recency_days is how
fresh a post must be to earn a drafted comment: only posts published within
that many days are commented on (default 7, max 7). Set these when the user
asks for a cadence ("once a week on Thursdays") or freshness rule ("only
comment on posts from the last 3 days") — those are the knobs, not the agent
goal text. A recency_days shorter than the gap between runs deliberately
skips the older posts in that gap: narrow it for a user who wants only the
freshest posts, or to trim a list monitor's per-post cost (see Cost — a
shorter window pulls fewer posts, though the per-profile floor dominates a
large list's cost). Leave it at the default week to comment on
everything since the last run. Omitting run_weekdays, recency_days, or (on a
list monitor) members on a re-setup keeps the monitor's current value; the
other fields overwrite on every re-setup, so re-pass
writing_instructions/draft_comments/fetch_engagers/capture_authors/
engager_icp/post_filter/engager_instructions when
editing rather than dropping them. For membership tweaks (add or drop a few people) use
edit_monitor_members — it takes only the changes, so you never re-echo the
whole list (dropping a URL on the re-echo silently stops watching that person).
Cost: a queued drafted comment costs 1 credit (all modes). list mode
additionally bills for the paid cookieless scraper's per-event usage
(1 credit = $0.10): 0.04 credit per post it returns plus 0.02 credit per
watched profile that posted nothing in the window — every watched profile is
billed on every scheduled run whether or not it posted, so the per-profile
floor (not the per-post charge) dominates a large list. That floor is a hard
minimum: a run costs at least 0.02 × number of watched profiles (a silent
profile exactly that, a posting one more), so a 2,000-profile list is ≥ 40
credits every scheduled day. Because that recurs per run, recommend a reduced
cadence
for a large list: the default is Mon/Thu, and for lists in the high hundreds
or more, suggest weekly (run_weekdays=[0]). Other levers are trimming the
member list; narrowing recency_days only trims the smaller per-post component.
Flag this to the user when setting up a list monitor, especially a big one;
a zero-balance list monitor won't run at all (even for engager capture).
network/topic discovery is free (Unipile session). Engager capture bills
0.04 credits per reaction, comment or reply each fetch pulls, and pulls a post
again once it's 5 days old, so its early engagers cost 0.08 credits each.
The capture actions share two optional companions, both handed to the woken
new_post_engagers run on the event. engager_icp filters WHO is collected,
engagers and authors alike (ICP, titles, exclusions); blank collects everyone. engager_instructions
decides WHAT HAPPENS to them; blank means they are recorded into the agent's
Output list and nothing else is done. Capture never sends outreach on its own —
if the user wants a sequence or a connection request, it has to be spelled out
in engager_instructions.
keywords is a LinkedIn boolean query. Combine topics with OR and quote
multi-word phrases: "creator economy" OR "influencer marketing". AND narrows,
NOT excludes, and parentheses group: ("seed" OR "series a") AND fundraising.
Operators must be UPPERCASE (lowercase and/or/not are read as literal words). The
combined AND+OR count is capped by the user's LinkedIn plan (free/premium 5, Sales
Navigator 15, Recruiter unlimited; NOT doesn't count) — past the cap LinkedIn
silently returns nothing, so cover many topics with several monitors each within
the cap, not one giant OR. A single unquoted topic (fundraising) is fine. A
malformed query (unbalanced quotes/parens, dangling operators) is rejected.
For network, leave keywords empty in almost all cases — the feed is already
ranked and the per-post draft step skips anything not worth commenting on; set a
narrow query only when the user wants one specific slice. list mode ignores
keywords — it watches the named people's and pages' recent posts directly, and the draft
step is the relevance gate. For mode='topic', keywords is required and is the
only filter.
Reusing an existing name updates that monitor (and re-activates it if it
was stopped) — this is also how you edit one. Prefer edit_monitor_members for
a list monitor's membership; reserve setup's members for the monitor's full
initial membership or a deliberate full reset.
Dict with status ('active' or 'error'), name, message, and additional keys.
Inputs: mode (required), name (required), agent_id (required), members, +11 more.
set_writing_style_guideSet writing style guideAInspect
Inputs: guide (required), channel (required), on_behalf_of.
start_find_searchStart find searchAInspect
Kick off an Exa Websets search for a named list on an agent.
An agent can host many named lists. Each list spawns its own Exa webset, so distinct criteria buckets (e.g. 'oil & gas operators' vs 'midstream') don't share Exa's per-webset domain-dedup.
Costs 2 Sliq credits per ingested result (charged during ingest). Free when the user has their own Exa key connected.
Refuses to start when the balance can't afford even one result in
sliq_credits mode. When the balance covers fewer results than requested,
the count is clamped to what's affordable and the result carries a
clamp_notice — surface it to the user ("you have X credits, so I'll
find Y").
Inputs: agent_id (required), criteria (required), list_name (required), entity_type (required), +8 more.
stop_linkedin_monitoringStop LinkedIn monitoringAInspect
Inputs: name (required), agent_id (required), on_behalf_of.
theirstack_searchTheirStack searchAInspect
Use to find companies hiring for a role / tech stack / headcount-band. The
return shape includes company.domain directly — feed that to
record_search_results as identifier without an Apollo round-trip.
For installed-base discovery — companies that already use a technology,
rather than hire around it — use find_companies_by_tech_stack instead.
Costs 0.5 Sliq credits per job posting returned; a failed search is free.
In chat, STATE THE COUNT AND THE COST in the same reply as the
results — every time, without stopping to ask first. Use the user's
number when they gave one, otherwise the default: "pulled 25
postings — 12.5 credits; say if you want more, max 100." Price the
postings actually returned, which count gives: fewer than limit
costs less.
On a scheduled scan there is no reply to state it in: pass the
number the user chose at setup, carried in the scan spec as
theirstack_limit.
Provide at least one of job_title_keywords, job_title_exact,
description_keywords, or industry — without some kind of scope filter,
searches return too many results. Empty results are normal; broaden
filters and call again.
Every call names its industry scope: industry, or the company_keywords
and company_description_keywords pair for a vertical the industry
catalog doesn't name; a call with none of the three raises ModelRetry.
When the target companies aren't defined by industry at all, pass
industry=[].
Each returned job's job_description is truncated to 250 chars (with an
ellipsis suffix). The full posting body is often 1-3KB; the agent only
needs a short excerpt for the signals[i].summary field, and the full
body would bloat the LLM context across multiple rounds.
Each company the postings belong to is then checked against criteria,
one company at a time, reading its name, firmographics and postings, and
only the postings at matching companies come back in jobs; the rest come
back in rejected as {name, reason}. It adds a few seconds per ten
companies.
When this runs in an agent, every company with a domain is saved to the
workspace Output tab as an agent_search_results company row (deduped by
domain, entity_type='company') with its verdict, carrying its hiring
postings in data.signals and its firmographics; the rejected ones are
hidden from the list's default view. Re-running updates rows in place.
Pass list_name to name their list; absent, they land in the 'default'
list.
A dict with jobs, the postings at matching companies; rejected,
the companies the check turned down, as {name, reason}; count,
every posting returned, rejected companies' included, which is the
number billed; and created_identifiers, the domains of the matching
companies this call newly added to the list, not ones already on it.
Each job carries job_title, job_description (truncated to 250 chars),
url, date_posted, a company dict (name, domain, industry,
employee_count, annual_revenue_usd, linkedin_url, country, city), and
its company's verdict ('qualified', or 'unclear' when the company's
details say too little to judge) and verdict_reason, with a null
verdict when the check couldn't run. On upstream failure (timeout /
5xx / auth / quota), it returns jobs=[], count=0, and
theirstack_available=False so the agent can degrade to other sources.
Inputs: criteria (required), limit, persist, agent_id, +18 more.
track_monitored_companiesTrack monitored companiesAInspect
Inputs: items (required), agent_id (required), on_behalf_of.
track_prospectsTrack prospectsAInspect
A person belongs to exactly one campaign, so someone already tracked in another
campaign is reported in skipped rather than added twice; pass move_existing=True
to move them into this agent instead. A durable contact left without a campaign (its
campaign was deleted) is instead adopted straight in and reported in adopted.
Someone already on THIS campaign who was removed from it is reported in removed,
not re-enrolled and not sent to. If the user explicitly wants them back, restore them with
update_prospect(agent_id=<this agent>, prospect_id=<their id from query_prospects>, channel=<'email' or 'linkedin', one they have a contact on>, stage='pending'). Someone
with no usable identifier (no LinkedIn profile URL, username slug, provider ID or email —
say, only a company page URL) is reported in unrecognized and not added; the rest of the
batch is.
On an agent with a define_sequence flow, tracking IS enrollment — the flow fires
each prospect's first touch itself (see the returned flow field); never queue a
first send for prospects you just tracked.
Dict with created and updated counts; moved, adopted, removed, skipped and
unrecognized details, plus unrecognized_note naming anyone not added; and
total, the number of items.
Inputs: items (required), agent_id (required), on_behalf_of, move_existing.
update_agentUpdate agentAInspect
An agent's recurring runs are steered by each trigger's prompt and code; the goal is shared context, not the run script. A goal edit that adds, removes, or changes a recurring step therefore doesn't reach the runs on its own: after updating the goal, read the agent's triggers and bring each affected trigger's prompt or code in line via update_trigger — or confirm the existing triggers already cover the change. Narrowing a live outreach campaign's ICP is the same shape with more surfaces: segment-bound triggers, per-segment templates, and queued-unsent sends stay wired to the dropped segment; follow your channel's outreach skill to reconcile them.
Set priority=True to push this whole campaign's queued sends — email and LinkedIn alike — ahead of your other campaigns'. It starves the others until it drains — that's intended. priority=False clears it.
Dict with success status and updated agent details
Inputs: agent_id (required), goal, title, status, +2 more.
update_calendar_eventUpdate calendar eventADestructiveInspect
Inputs: calendar_event_id (required), body, subject, location, +4 more.
update_crm_taskUpdate CRM taskAInspect
Inputs: task_id (required), clear, notes, title, +9 more.
update_dealUpdate dealAInspect
Won and lost are statuses, not stages: use status='won' / 'lost', and status='open' to reopen. Deleting (archived=true) hides the deal everywhere but keeps it, so archived=false brings it back. The updated deal, same shape as a query_deals row.
Inputs: deal_id (required), name, clear, notes, +12 more.
update_message_tagUpdate message tagAInspect
Inputs: tag_id (required), name, description, on_behalf_of.
update_message_tag_groupUpdate message tag groupAInspect
Inputs: group_id (required), mode, name, criteria, +2 more.
update_monitored_companyUpdate monitored companyAInspect
Inputs: agent_id (required), identifier (required), stage, data_updates, +1 more.
update_nodeUpdate nodeAInspect
Because no node is added, dropped, or renamed, every prospect keeps its cursor and each
node keeps its on_enter enactment (prompt, code, run history). The edited sequence is
re-validated as a whole, so an edge repoint that would dangle a reference, orphan a node,
or introduce a cycle is rejected rather than saved. To add or remove nodes, change a
node's kind, or edit the start node, re-author the flow with define_sequence instead.
A forked-on condition is often restated in several places — a decision's rule, each
arm.case, a downstream terminal's label, and the node's on_enter prompt. When you
change one, change the others in the same turn (a further update_node for the node
fields, update_trigger for the prompt) so they agree.
Dict with success, agent_id, and node_id. Read the updated flow via
get_campaign_flow.
Inputs: node (required), agent_id (required), on_behalf_of.
update_onboarding_progressUpdate onboarding progressBInspect
Inputs: milestone (required), current_task, on_behalf_of.
update_pageUpdate pageAInspect
Inputs: page_id (required), code, name, archived, +3 more.
update_prospectUpdate prospectAInspect
Set priority=True to push this person's queued sends — email and LinkedIn alike —
ahead of everyone else's; it's account-wide and durable, carrying across the sequence's
later steps for this person. priority=False clears it. One priority flag per prospect,
independent of channel: both channels' derived send order reads it.
Most stage transitions — sent, connected, messaged, replied — stamp
automatically from send and reply detection; don't set those by hand. The three
reply-outcome rungs above replied — interested/meeting_booked (positive) and
not_interested (an explicit decline) — normally stamp from the reply classifier
too, so set one by hand only to fix a wrong call or record an off-platform outcome:
stage='interested' for clear interest, stage='meeting_booked' when they agree to or
book a meeting, stage='not_interested' when they've declined. The mark only raises a
rung — a decline can't override a live positive (to lower a false
meeting_booked/interested, correct that reply with correct_reply_outcome) — and it
counts the person as replied, so to drop someone who never engaged use stage='skipped'.
Setting a reply-outcome rung also cancels the person's still-pending queued outreach
on both channels — automation stops; messages you queue afterwards still send.
A later positive reply lifts not_interested back up on its own.
stage='skipped' removes the person from this campaign on both channels, whatever
channel you pass — the same as the user's Remove. Their queued sends are cancelled and
the campaign's later steps stop for them; their stages keep what already happened, so
the campaign's funnel still counts a reply they sent. When the campaign contacts them on
a channel the user didn't ask to stop — its sequence has a step on that channel, or
their query_prospects row shows a stage there (email_stage, linkedin_stage) —
first tell the user the removal stops that channel too and wait for their go-ahead. Pass
skip_note with the reason in a few words, so a later reader knows why this person was
dropped without digging up the chat — a bare skip records no reason.
Any other stage on a removed person (removed_at set on their query_prospects row),
except a reply stage (replied or a rung above it), puts them back in the campaign on
both channels; a reply stage records what they said and leaves them removed. When
putting them back would restart a channel the user wants kept stopped, such as email for
someone who opted out of it, first tell the user and wait for their go-ahead. To
restore, pass stage='pending' with a channel they have a contact for on that row
(email, or linkedin_slug/linkedin_provider_id). That channel takes 'pending' only
if nothing went out on it; a channel already contacted keeps its stage. Restoring clears
the stale skip note, and their flow position is preserved, so the sequence resumes from
where they were rather than restarting a step already taken.
To store an address you found for a prospect that reached an email step without
one (recovering an email_not_found block), pass email; the stamped address makes
the step send when you retry it. For a blocked prospect,
retry_blocked_enactment(email=...) stamps and retries in one call instead.
When the user tells you where a prospect is, or you learn it (their company's office, an
email signature, a past call), and it differs from the timezone query_prospects shows for
them (blank when unknown), pass timezone. A send to them then goes out only inside the
user's outreach hours on both clocks, theirs and the user's; when those hours never overlap,
it keeps to the user's own.
To undo an accidental hand-set mark (e.g. a prospect wrongly marked meeting_booked),
pass clear_manual_mark=True on channel — it removes the mark and re-derives the
stage from actual send/reply activity, the only way to lower past a manual mark (which
the mark path deliberately floors demotions at). This differs from correct_reply_outcome,
which re-judges a specific reply; use clear_manual_mark when the wrong rung came from a
hand mark, not a misjudged reply. Pass it on its own (no stage/data/priority/email).
Dict with the updated item details. After a skip, skip_outcome says, for each
channel where it cancelled queued sends or found some already sent, how many it
cancelled and which already went out, which a skip can't undo. It also names the gift cards awaiting the user's approval
that the skip cancelled, and each gift it left in place: one ordered or sent as a
link. Tell the user about each of these. If the user wants a gift left in place gone,
cancel_gift cancels one nothing was ordered for. After a timezone write,
send_hours says when sends to them now go out, on their clock and the user's. Give
the user those hours as it states them rather than working them out.
Inputs: channel (required), agent_id (required), prospect_id (required), email, +8 more.
update_segment_groupUpdate segment groupAInspect
Inputs: group_id (required), mode, name, criteria, +2 more.
update_segment_tagUpdate segment tagAInspect
Inputs: tag_id (required), name, description, on_behalf_of.
update_triggerUpdate triggerAInspect
A send step's message, or a connection request's note, lives in its message_templates, so
to change what a step sends,
edit the template with update_node; an on_enter prompt on that step carries only
extra instructions (tone, whether it may rephrase, what to write when a {slot} can't be
filled: wording the user could approve without changing a word, since the queue rejects
a leftover {slot}), never the message itself. A [[double-bracketed]] part of a template
is the user's to fill in while approving, so the run leaves it as written.
You MUST call get_skill_guide('trigger_code') before writing code — the
qualification rules, sandbox globals, tool surface, and out contract live
in the skill. New or changed code runs on the trigger's next firing.
In chat, if this edit leaves the goal out of date, update the goal to match in
the same turn with update_agent(agent_id=..., goal=...). On a background run,
name the mismatch in your results report instead.
Dict with success, agent_id, trigger_id, next_run_at, and the edited trigger's
full dict under trigger, plus a warning when an active trigger sits on a paused
agent, since it doesn't fire until the agent is set back to active.
Read the agent's other triggers via get_agent_details.
Inputs: trigger_id (required), code, label, prompt, +4 more.
update_workspaceUpdate workspaceAInspect
Use this for structured data. To add an agent-specific operational learning, call
append_learning instead.
Outputs are auto-rendered by type — use separate keys for different data types. Numbers render as stat cards, arrays of objects render as tables, strings render as markdown. Use real types (actual numbers, actual arrays), not stringified JSON inside markdown. Keep table objects flat. Omit internal IDs (provider_id, etc.) from outputs unless user-facing.
The app reads inputs: excluded, mailbox, entity_type, discovery_mode, watcher_context, content_calendar, destination; outputs: ideas, drafts, published, digests, insights. Values there must fit the app's shape (a rejected write shows it); for your own data, use another key.
operation='read': Name section and key to get back just that entry; section alone returns that section; neither returns the full workspace ({inputs, outputs}). An entry that was never written comes back as an empty section. The full form re-sends every entry, so name the entry when you know which one you want. After an update mid-turn, read the entry back here: the workspace copy in your prompt is stale until the next turn. operation='update': Requires section, key, and a non-null value. Sets workspace[section][key] = value. operation='append': Requires section, key, and a LIST value. Atomically extends the existing list at workspace[section][key] with value's items (creating it if absent). Use this to accumulate into a list — a digest entry, new calendar topics, freshly-created draft records — without reading, concatenating, and rewriting the whole array yourself (which races other writers). Errors if the current value isn't a list (use 'update' to replace it). operation='delete': Requires section and key. ALWAYS confirm with the user before calling — this is destructive. Shape depends on the operation. On read, {'success': True, 'agent_id': ..., 'workspace': {'inputs': {...}, 'outputs': {...}}}, with 'workspace' holding only the section — or only the one section/key entry — you named. On update or delete, {'success': True, 'agent_id': ..., 'workspace_keys': {'inputs': [...], 'outputs': [...]}}
Inputs: agent_id (required), operation (required), key, value, +2 more.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
Related MCP Connectors
LinkedIn outreach from Claude with a human veto: review, approve and send drafts, triage replies.
Find LinkedIn prospects, draft outreach, and run human-paced campaigns from your AI agent.
LinkedIn outreach from Claude: invites under safe caps, follow-ups, replies and inbox triage.
Give AI agents the LinkedIn tools to find, qualify, engage, and follow up with prospects.
Related MCP Servers
- AlicenseAqualityBmaintenanceAI agent for LinkedIn outreach: finds the right people, writes in your voice, follows up, replies. Six campaign goals: sell a product or service, find a job, hire people, find partners or investors, find a vendor, research interviews. Campaigns stay drafts until you launch them.222,088 PyPI13MIT
- AlicenseNot gradedqualityDmaintenanceEnables running the entire LinkedIn sales motion inside Claude Code, including content creation, audience warming, outreach, and booking calls.9MIT

GTM Bud MCP connectorofficial
AlicenseNot gradedqualityCmaintenanceEnables AI assistants to build, preview, launch, and monitor LinkedIn outbound campaigns, draft replies for approval, manage campaign settings, and upgrade profiles.1MIT
PumpGTM MCP serverofficial
AlicenseNot gradedqualityBmaintenanceEnables AI agents to find intent-signaling buyers and run LinkedIn, email, and X outreach from connected accounts while respecting platform limits. Every reply is handed back to a human for approval or decision.3MIT
Glama MCP Gateway
Add one secure layer between your agents and this server.