Skip to main content
Glama

Server Details

Run LinkedIn and email outbound from Claude, ChatGPT, or any AI agent.

Ownership verified

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

A3.6/5.0

Scored across 177 tools

Disambiguation3/5

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.

Naming Consistency4/5

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.

Tool Count1/5

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.

Completeness4/5

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 tools
add_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 enrichA
Read-only
Inspect

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 saveA
Destructive
Inspect

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.

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):

  1. 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=[...]) (or track_monitored_companies for a monitored company) for per-entity lifecycle tracking, OR → update_workspace(section='outputs', key='<your_list>', value=[...]) to store as an agent deliverable.

  2. 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.

  3. 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 giftA
Destructive
Inspect

Inputs: gift_id (required), agent_id (required), on_behalf_of.

check_email_daily_usageCheck email daily usageA
Read-only
Inspect

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 usageA
Read-only
Inspect

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 eventA
Destructive
Inspect

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"). entry is 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_request node (LinkedIn): {id, label, kind, accepted, already_connected, timeout?, message_templates?}. message_templates holds its note (see below); without one the step's on_enter code 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's then — {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. Route already_connected to a terminal by 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. timeout is an optional held advance {delay: {value, unit}, to} off an unaccepted request.

  • send node (a repliable send — LinkedIn message / inmail, or an email): {id, label, kind, channel, action, replied, follow_up?, message_templates?}. channel is linkedin or email (so one sequence spans both); action is a LinkedIn verb for channel linkedin or email for channel email. Routes replied (required) so a reply advances the prospect off the node; follow_up is an optional held no-reply follow-on {delay, to}. message_templates holds the step's message (see below).

  • action node (a bodyless LinkedIn action — follow / resolve / comment / reaction): {id, label, kind: "action", channel: "linkedin", action, then}. Nothing to reply to; then is 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. A resolve action may add notify: 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.

  • manual node (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_description is 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. then is 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). Use manual ONLY 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 an automation node, and an automatable outreach touch is an action/send node.

  • gift node (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) and max_price_usd (the most the item may cost including tax and shipping, in whole US dollars) are optional defaults pre-filled on every card, and gift_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 as product_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 most max_price_usd, plus about $3.30 + 2.9% in fees on top. Set a budget the user names for each gift as max_price_usd as 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. then is 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.

  • instantly node (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 its id from instantly_list_campaigns as campaign_id and its name as label, which names the campaign wherever the step shows. There is nothing to write: the Instantly campaign holds the emails. replied is a bare node id, like a send's replied. no_reply is a required held advance {delay, to}, like a send's follow_up: once Instantly has finished the campaign's sequence, a prospect who hasn't replied moves on when delay has passed since Instantly's last email to them, so delay is 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.

  • automation node (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. then is the next node's id as a bare string, not the {to} object a manual, gift or silent action step's then takes. Author the side-effect itself onto this node's on_enter trigger with update_trigger (a prompt, or code for 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. Unlike manual it boots an agent run and needs no human; unlike a terminal hook it advances. Reach for this when the user wants a step that "does something" automatically mid-sequence and then continues.

  • decision node: {id, label, kind, rule, arms}. rule is the NL fork judgment; arms is a list of {case, to} — each case a 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.

  • terminal node: {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 agentA
Destructive
Inspect

Inputs: agent_id (required), on_behalf_of.

delete_message_tagDelete message tagA
Destructive
Inspect

Inputs: tag_id (required), on_behalf_of.

delete_message_tag_groupDelete message tag groupA
Destructive
Inspect

Inputs: group_id (required), on_behalf_of.

delete_segment_groupDelete segment groupA
Destructive
Inspect

Inputs: group_id (required), on_behalf_of.

delete_segment_tagDelete segment tagA
Destructive
Inspect

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 profilesA
Read-only
Inspect

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 scopeA
Read-only
Inspect

Inputs: channel, senders, agent_ids, positions, +2 more.

estimate_segment_classify_scopeEstimate segment classify scopeA
Read-only
Inspect

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.

fetch_linkedin_messages_with_personFetch LinkedIn messages with personA
Read-only
Inspect

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 emailA
Read-only
Inspect

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 URLA
Read-only
Inspect

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 numberA
Read-only
Inspect

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 pathsA
Read-only
Inspect

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 commentA
Destructive
Inspect

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 detailsA
Read-only
Inspect

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 flowA
Read-only
Inspect

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 messagesA
Read-only
Inspect

Inputs: session_id (required), limit, offset, on_behalf_of.

get_credit_usageGet credit usageA
Read-only
Inspect

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 attachmentA
Read-only
Inspect

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 emailsA
Read-only
Inspect

Inputs: ids (required), on_behalf_of.

get_integration_statusGet integration statusA
Read-only
Inspect

Inputs: on_behalf_of.

get_linkedin_monitorsGet LinkedIn monitorsA
Read-only
Inspect

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 postsA
Read-only
Inspect

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/comments are engagement COUNTS, not the people who engaged — use fetch_post_engagers for 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_found means the actor could not resolve the target (renamed/private/deleted); no_recent_posts means it resolved but nothing fell in the window. (Distinct from the top-level unresolvable_profiles list 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 eventsA
Read-only
Inspect

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 guestsA
Read-only
Inspect

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 deliverabilityA
Read-only
Inspect

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 callsA
Read-only
Inspect

Inputs: agent_id (required), change_id (required), limit, offset, +1 more.

get_meeting_transcriptGet meeting transcriptA
Read-only
Inspect

Inputs: meeting_id (required), on_behalf_of.

get_message_experimentsGet message experimentsA
Read-only
Inspect

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 ratesA
Read-only
Inspect

Inputs: group_id (required), origin, channel, senders, +2 more.

get_message_tag_unclassified_countGet message tag unclassified countA
Read-only
Inspect

Inputs: group_id (required), on_behalf_of.

get_node_historyGet node historyA
Read-only
Inspect

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 approvalA
Read-only
Inspect

Inputs: agent_id, on_behalf_of.

get_pageGet pageA
Read-only
Inspect

Inputs: page_id (required), version, on_behalf_of.

get_pending_approvalsGet pending approvalsA
Read-only
Inspect

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 guideA
Read-only
Inspect

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 transcriptA
Read-only
Inspect

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 funnelA
Read-only
Inspect

Inputs: agent_id (required), group_id, on_behalf_of.

get_segment_ratesGet segment ratesA
Read-only
Inspect

Inputs: group_id (required), channel, senders, agent_ids, +1 more.

get_segment_unclassified_countGet segment unclassified countA
Read-only
Inspect

Inputs: group_id (required), on_behalf_of.

get_settingsGet settingsA
Read-only
Inspect

Inputs: section (required), on_behalf_of.

get_skill_guideGet skill guideA
Read-only
Inspect

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 tabA
Read-only
Inspect

Inputs: col_kind (required), row_kind (required), col_group_id (required), row_group_id (required), +5 more.

get_tool_connect_urlGet tool connect URLA
Read-only
Inspect

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 memoryA
Read-only
Inspect

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 guideA
Read-only
Inspect

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.

import_linkedin_historyImport LinkedIn historyAInspect

Inputs: window (required), on_behalf_of.

list_agency_clientsList agency clientsA
Read-only
Inspect

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 agentsA
Read-only
Inspect

Inputs: on_behalf_of.

list_attachmentsList attachmentsA
Read-only
Inspect

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 campaignsA
Read-only
Inspect

Inputs: on_behalf_of.

list_companiesList companiesA
Read-only
Inspect

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_count and people_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 with company_id and no agent_id.

  • signals: {count, latest, monitored}: how many signals the company has, the newest one, and whether a monitor watches it. With agent_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 with include_signals: every one of those signals, newest first.

With agent_id, each row also has:

  • lists and sources: the search lists and discovery tools that surfaced the company, empty for one the agent only monitors. columns holds the researched [label, value] pairs.

  • verdict, verdict_reason and rejected_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 a group_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_on is {lists, reason}: the lists whose own verdict is 'rejected' while the company's verdict isn't, with the latest rejected row's reason; its lists is 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 or include_rejected is set, the same as the user's default view.

  • criteria, only with include_criteria: why the agent matched the company, as its opened row shows it. Per list it's in, {list_name, evaluations}; each evaluation is a criterion with the reasoning, a verdict (satisfied 'yes', 'no' or 'unclear', or a numeric score where 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 conversationsA
Read-only
Inspect

Inputs: tab, limit, offset, agent_id, +1 more.

list_message_tag_groupsList message tag groupsA
Read-only
Inspect

Inputs: on_behalf_of.

list_pagesList pagesA
Read-only
Inspect

Inputs: archived, on_behalf_of.

list_peopleList peopleA
Read-only
Inspect

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}, where removed is 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. state is one of not_checked, checking, queued, found, cold, connected, unresolved. connectors lists the user's 1st-degree connections who know the person (name, headline, identifier, provider_id). mutual_connections_truncated true means connectors is a sample of a longer list. shared_connections_count is LinkedIn's total. result_id is 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_stage and linkedin_stage are their prospect row on this agent, null when not enrolled. removed is true when they were removed from the agent's campaign, and removal_reason says why, '' when they weren't or no reason was recorded.

  • lists and sources: the search lists and discovery tools that surfaced the person. columns holds the researched [label, value] pairs.

  • verdict, verdict_reason and rejected_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 a group_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_on is {lists, reason}: the lists whose own verdict is 'rejected' while the person's verdict isn't, with the latest rejected row's reason; its lists is 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 or include_rejected is set — the same as the user's default view — though a tracked prospect always shows.

  • criteria, only with include_criteria: why the person matched, as their opened row shows it. Per list they're in, {list_name, evaluations}; each evaluation is a criterion with the reasoning, a verdict (satisfied 'yes', 'no' or 'unclear', or a numeric score where 7 and up is met) and http(s) references. Criteria run about 2KB per person, so ask for them on a narrow read (a q, one bucket, or a small limit) 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 eventsA
Read-only
Inspect

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 groupsA
Read-only
Inspect

Inputs: on_behalf_of.

list_sent_messagesList sent messagesA
Read-only
Inspect

Inputs: q, limit, offset, origin, +7 more.

list_teammatesList teammatesA
Read-only
Inspect

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.

list_team_shared_agentsList team shared agentsA
Read-only
Inspect

Your active teammates' live agents, grouped by teammate email.

Returns {'teammates_agents': {teammate_email: [{'id', 'title', 'status', 'template_id', 'has_sequence'}, ...], ...}}. Every active teammate is a key, with an empty list when they run no agents. A user with no team gets {'teammates_agents': {}}.

Inputs: on_behalf_of.

look_up_imported_peopleLook up imported peopleAInspect

Inputs: senders, on_behalf_of.

manage_email_outreach_queueManage email outreach queueA
Destructive
Inspect

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 queueA
Destructive
Inspect

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 ICPA
Read-only
Inspect

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_included True → Apollo had no headcount; included anyway.

  • size_contested True → 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

  1. 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 from location_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 analyticsC
Read-only
Inspect

Inputs: metric (required), days, custom_query, on_behalf_of.

query_companiesQuery companiesA
Read-only
Inspect

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 tasksA
Read-only
Inspect

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 dealsA
Read-only
Inspect

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 engagementsA
Read-only
Inspect

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 postsA
Read-only
Inspect

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 companiesA
Read-only
Inspect

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 postsA
Read-only
Inspect

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 peopleA
Read-only
Inspect

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 researchA
Read-only
Inspect

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 prospectsA
Read-only
Inspect

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 resultsA
Read-only
Inspect

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 runsA
Read-only
Inspect

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 peopleA
Read-only
Inspect

Inputs: group_id (required), limit, offset, tag_id, +6 more.

query_suggested_dealsQuery suggested dealsA
Read-only
Inspect

Inputs: limit, offset, removed, on_behalf_of.

query_tagged_messagesQuery tagged messagesA
Read-only
Inspect

Inputs: group_id (required), limit, offset, origin, +8 more.

queue_email_outreachQueue email outreachA
Destructive
Inspect

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 outreachA
Destructive
Inspect

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 engagementA
Destructive
Inspect

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 attachmentA
Read-only
Inspect

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 attachmentA
Read-only
Inspect

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 attachmentA
Read-only
Inspect

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 dateA
Read-only
Inspect

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 calendarB
Read-only
Inspect

Inputs: where_clause (required), limit, offset, order_by, +1 more.

search_chat_historySearch chat historyA
Read-only
Inspect

Inputs: limit, keyword, on_behalf_of, created_after, +1 more.

search_emailsSearch emailsA
Read-only
Inspect

Inputs: where_clause (required), limit, offset, order_by, +1 more.

search_linkedin_connectionsSearch LinkedIn connectionsA
Read-only
Inspect

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 historyA
Read-only
Inspect

Inputs: where_clause (required), limit, offset, order_by, +1 more.

search_linkedin_peopleSearch LinkedIn peopleA
Read-only
Inspect

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 their provider_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 in connections_of and the target company in advanced_keywords={'company': 'Acme Corp'} to surface who they know there. network_distance is 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 of first_name, last_name, title, company, school (each a string). company matches 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 meetingsA
Read-only
Inspect

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 eventsA
Read-only
Inspect

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 emailA
Destructive
Inspect

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 invitesA
Destructive
Inspect

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. settings is 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 settingA
Destructive
Inspect

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.

stop_linkedin_monitoringStop LinkedIn monitoringAInspect

Inputs: name (required), agent_id (required), on_behalf_of.

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 eventA
Destructive
Inspect

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

    Related MCP Servers

    • A
      license
      A
      quality
      B
      maintenance
      AI 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.
      22
      2,088 PyPI
      13
      MIT
    • A
      license
      Not graded
      quality
      C
      maintenance
      Enables AI assistants to build, preview, launch, and monitor LinkedIn outbound campaigns, draft replies for approval, manage campaign settings, and upgrade profiles.
      1
      MIT
    • A
      license
      Not graded
      quality
      B
      maintenance
      Enables 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.
      3
      MIT
    Try in Browser

    Glama MCP Gateway

    Add one secure layer between your agents and this server.

    Resources