Signals
Capture inbound intent events about people and companies, rank who to contact today, and keep signal tables refilling on a cadence.
A signal is one inbound intent event about a person or company: someone visited your site, viewed your profile, reacted to your post, or followed you. oxygen signals leads-today ranks them into a contact list.
The type registry
Four types are registered, all captured end to end, each with an intent weight that drives ranking:
| Type | Weight | Captured by |
|---|---|---|
website_visit | 100 | RB2B de-anonymization webhook |
profile_view | 60 | LinkedIn profile-viewer import |
post_reaction | 40 | LinkedIn engagement harvest |
new_follower | 20 | LinkedIn follower import |
oxygen signals registry --jsonSignals span three source families in the product model — pulse (external market events), engagement (inbound interaction), product (in-app usage). Only engagement is registered today: signals registry returns families: ["engagement"].
Three limits apply to every row above:
signals recordand the RB2B webhook only land if the matching signal-touch automation —crm-website-visitor,crm-profile-viewer,crm-post-engager,crm-follower— is armed in the workspace. Otherwise the event dispatches with zero deliveries and nothing reaches the timeline.- The LinkedIn imports raise signals only on people who already have a CRM record: the bridge that drains them never creates one.
- There is no
post_commenttype. A comment on a post is stored aspost_reactionwithengagement_type=comment.
Tag a type to file it with a campaign. A tag is a label only; it changes nothing about capture:
oxygen tags attach signal_type website_visit --tags q3-outbound
oxygen signals registry --tag q3-outbound --jsonOn /signals, open a type to edit its tags; /signals?tag=q3-outbound lists only the types carrying that tag. See Tags.
Where you see them
/signals (by URL or ⌘K) lists the signal types and their tags, not the events. The events reach the browser in two places:
- CRM. Open a person from CRM in the sidebar (
/crm), then use the Signals filter on the record's Activity tab —/crm/people/<rowId>?tab=activity. Each event renders as a timeline entry. - Tables (
/tables). Every import and harvest fills a workspace table you can enrich, filter, and enroll from.
The ranked queue is CLI/MCP only:
oxygen signals leads-today --within-days 7 --limit 25 --json
oxygen signals list --types website_visit,post_reaction --since 14 --jsonsignals list returns up to your plan's feed limit: 100 events over 30 days on free, rising to 1,000 events over 365 days on the largest plans (table and signal limits; oxygen limits show names yours). A larger --limit or --since is refused with the limit named, not cut short.
leads-today ranks by weight, then recency. Do-not-contact filtering is best-effort: suppression keys on LinkedIn provider ids, so an email-only person (an RB2B reveal) comes back with suppressionResolvable: false — surfaced unchecked, not cleared.
Getting signals in
One command arms standing capture of profile viewers, followers, and connections:
oxygen linkedin intent setup --account <sender> --json
oxygen linkedin intent status --jsonAdd --post <social_id> to also watch one post's reactions and comments. Nothing is sent and no credits are charged.
The imports fill the same tables and are free as well. Each stays on and keeps its table fresh in the background; oxygen viewers pause --account <sender> switches the viewers capture off for one account:
oxygen viewers import --json
oxygen followers import --json
oxygen connections import --jsonEach drips in the background under a conservative read budget; poll viewers status, followers status, connections status. LinkedIn hides anonymous viewers, so the viewers table is always a partial sample.
Enriching each new profile viewer's full profile spends credits on every viewer it adds. The CLI leaves it off unless you run oxygen viewers enrichment --on or viewers import --enrich; viewers status shows the per-viewer price. In the web app, Enrich each new viewer's full profile starts ticked when you first switch on profile viewer capture; untick it to keep the capture free.
Harvesting one post's engagers spends credits when it runs cookieless (a public post URL through the managed scraper), so --max-credits is required:
oxygen engagement harvest --post <social_id_or_url> --max-credits 200 --json
oxygen engagement status --jsonWriting a signal yourself is free and idempotent on --external-event-id:
oxygen signals record --event website_visit --email [email protected] \
--company-domain acme.com --page-url https://you.com/pricing --jsonEvents are website_visit, profile_view, post_engager, new_follower — post_engager stores as the registered post_reaction type. Identity comes from --email or --linkedin-url; --company-name, --first-name, --last-name, --session-id, and --payload-json fill the rest.
RB2B has no self-serve connect step yet: OXYGEN sets up its webhook for your workspace, so ask support to connect it. From then on each reveal is a website_visit signal, and oxygen feeds bind --source rb2b --create "Website visitors" also lands the reveals as table rows at 0 credits. To bring in visits from another visitor-identification tool, send each one with signals record.
LinkedIn capture tables
In the web app these are the Professional Network cards under New table: Connections & Followers, Open Connection Requests, Profile Viewers, Post Engagers, Post Search, and Company Followers (an order fulfilled by ScrapeLi). The first three read your own connected accounts and cost no credits apart from the optional viewer enrichment above; they are paced against each account's daily LinkedIn read budget.
Your network
oxygen linkedin network setup --json
oxygen linkedin network status --json
oxygen linkedin network pause --account <sender> --jsonsetup keeps one LinkedIn Network table of everyone you are connected to and everyone who follows you, one row per person; Relationship reads connection, follower, or both. It covers every connected account, or one with --account, and arms accounts you connect later automatically. A newly connected account eases into full reading speed over its first two weeks, so a large network lands over days; status shows each account's pace. pause stops reading and keeps the table, and setup continues where it stopped. MCP: oxygen_linkedin_network_setup, oxygen_linkedin_network_status, oxygen_linkedin_network_pause.
Open connection requests
oxygen linkedin invitations setup --json
oxygen linkedin invitations status --json
oxygen linkedin invitations list --account <sender> --json
oxygen linkedin invitations withdraw --older-than 30 --account <sender> --json # preview
oxygen linkedin invitations withdraw --older-than 30 --account <sender> --approved --json # withdrawsetup keeps one LinkedIn Open Connection Requests table: every pending request on your accounts, received and sent, with its direction and status. Each account is checked about twice a day within its working hours. A request that disappears becomes accepted when the person is now a connection, otherwise no_longer_pending, because LinkedIn does not say whether it was declined, withdrawn, or expired. The table only reads LinkedIn and costs no credits. Accounts you connect later stay off until you run setup for them, and pause stops checking.
withdraw is the one command here that acts on LinkedIn: it previews the target list and withdraws only with --approved. Pick targets with --ids from invitations list, or with --older-than <days> among your 100 most recent sent requests. MCP: oxygen_linkedin_invitations_setup, oxygen_linkedin_invitations_status, oxygen_linkedin_invitations_pause, oxygen_linkedin_invites_sent, oxygen_linkedin_invites_withdraw.
Post Engagers
oxygen tables watcher preview --profiles-json '["https://www.linkedin.com/in/example"]' --json
oxygen tables watcher create --profiles-json '["https://www.linkedin.com/in/example"]' \
--max-credits <credits> --preview-hash <hash> --request-id <uuid> --approved --json
oxygen tables watcher pause --table <table> --jsonA Post Engagers table watches 1–10 public LinkedIn profiles — your team's, partners', or competitors' — and adds the people who react to or comment on their posts from the last seven days, with the post they engaged with and their employer. It runs when you create it and then daily at 07:00 UTC, collecting up to 500 reactions and 500 comments per post and enriching up to 40 new people per cycle.
The table fills rows and raises no signals: a reaction or comment it collects never reaches signals leads-today or a record's Signals filter, and autoenroll does not cover it.
Each cycle spends credits within the per-cycle --max-credits you approve. preview is free: it quotes the credits per cycle and suggests a cap. create needs that preview's --preview-hash, --approved, and a --request-id UUID that you reuse if you have to retry. update and resume ask for a fresh preview and approval; pause keeps the table and its rows. To run it on your own Up2Data key, see LinkedIn data access. MCP: oxygen_tables_watcher.
Enroll captured people automatically
oxygen linkedin intent autoenroll --account <sender> --sequence <sequence> --json # preview
oxygen linkedin intent autoenroll --account <sender> --sequence <sequence> --approved --json # arm
oxygen linkedin intent autoenroll --account <sender> --disable --jsonA capture only fills tables until you let it contact people. autoenroll grants that for one sender: every later cycle enrolls newly captured people into the sequence you name, under the caps you approve, without asking again. It covers only that sender's LinkedIn captures, not a Post Engagers table, and it does not check a fit score: people are enrolled whether or not they fit your ICP. Run linkedin intent setup first. Without --approved it writes nothing and shows the plan: the sender's daily caps, the captures it covers (--kinds: connections, followers, profile_viewers, own_posts, post; by default the ones setup already armed), the audience split (already connected, needs an invite, on your do-not-contact list, owned by another sender), and a seven-day send forecast. People on your do-not-contact list are never enrolled.
By default the first cycle records everyone already captured as a baseline and enrolls only people who connect, follow, view, or engage from then on; add --include-existing-network to reach your existing connections too. --max-enrolls-per-day defaults to the sender's daily message cap, and a sender still on its warm-up ramp is held to 5 invites and 5 messages a day for its first three days. --max-credits-per-cycle bounds what one cycle may spend and defaults to your plan's per-delivery ceiling: reading through your connected account is free, a cookieless post harvest is not.
If the sequence's source table requires a sign-off, nobody is enrolled until it is approved; people captured in the meantime stay in the table for you to enroll by hand. --disable revokes the grant and needs no approval; the captures keep filling their tables. MCP: oxygen_linkedin_intent_autoenroll.
Feeds
A feed is a scheduled provider harvest bound to a table, so the table keeps filling itself. linkedin intent setup creates them; you operate them like any other.
oxygen feeds list --json
oxygen feeds get <feed> --json
oxygen feeds pause <feed>
oxygen feeds resume <feed>
oxygen feeds unbind <feed>Pause is free and total: the cadence stays armed but every cycle is refused, so nothing is fetched and nothing is billed. unbind deletes the feed and keeps every row it wrote. In the browser a bound table carries a Live, Paused, Error, or Exhausted chip beside its name on /tables/<id>, a pause switch next to the chip, and View deliveries in the chip's menu.
oxygen feeds run <feed> defaults to --mode dry_run, which renders the exact next provider request — cursor already injected — for free. A live cycle spends credits:
oxygen feeds run <feed> --mode dry_run --json
oxygen feeds run <feed> --mode live --max-credits 50 --approvedLinkedIn capture feeds refuse feeds run with feed_not_scheduled: they cycle on the worker's claim loop, not a cron, so there is no scheduled cycle to fire by hand.
oxygen feeds bind <table> arms a new one from a plan route's --kind, --tool-id, and --request-json. Adding a cadence on a paid provider is a standing spend grant and needs --every, --max-credits, and --approved together; without --every the feed is stored disarmed.
Market signal search
Plan first. This is free and calls no provider:
oxygen signals search plan --family hiring --prompt "companies hiring SDRs" \
--countries US --last-days 30 --estimate --jsonFamilies are hiring, tech, funding, acquisition, news, job_change, and linkedin_posts (public LinkedIn posts matching your keywords); scope is market or watch_list with --domains. The plan returns the provider chain, which filters were dropped and why, a credit estimate, and the table blueprint.
Then run. Dry run is the default; live needs both gates:
oxygen signals search run --plan-json plan.json --json
oxygen signals search run --plan-json plan.json --mode live --max-credits 200 --approvedAdd --bind-feed --every weekly@mon9 --max-credits-per-cycle 50 to the live call to bind the route as a feed at the same time. Cadences are hourly, every_6h, daily@<hour>, or weekly@<day><hour>, in UTC.
Signal search is available through the CLI and MCP. In the new-table picker only Job postings and Professional Network Post Search are offered; the tech, funding, acquisition, news, and job-change cards are hidden while the browser setup is redesigned.
LinkedIn Post Search
oxygen signals search preview --keywords "cold email,outbound" --exclude-keywords hiring \
--last-days 7 --sort top --ai-filter "founders sharing outbound results" --json
oxygen signals search run --family linkedin_posts --keywords "cold email,outbound" \
--exclude-keywords hiring --last-days 7 --sort top --mode live --approved --json
oxygen signals search filters --source linkedin_posts --jsonlinkedin_posts finds public LinkedIn posts by keyword, one row per post with its author and engagement. A post must mention at least one --keywords entry (at most five, each its own search), every --all-keywords entry, and none of --exclude-keywords. --last-days (1, 7, 30, or 3650 for any time; default 30), --sort (recent, relevance, or top), --min-reactions, and --posted-by (a profile or company page URL) narrow it further, and filters lists them all. --ai-filter keeps only the posts an AI judges to match your description, and each kept post says why.
preview is free and shows up to 25 posts per keyword; it always runs on OXYGEN's managed key. On the managed key a live run needs only --approved and costs no credits, unless you add --ai-filter: then each kept post's Why it matches reason costs credits (at most 1.5 credits per post fetched, charged only for kept posts), so the dry run quotes it and the live run also needs --max-credits. --credential-mode user_api_key searches on your own Up2Data key instead; Up2Data bills the searches, OXYGEN adds its own-key platform fee per page, and the run needs --max-credits too (LinkedIn data access). In the web app this is New table → Professional Network Post Search, with a free sample beside the filters. MCP: oxygen_signals_search_preview, oxygen_signals_search_run.
A signal-search table is table data, not stream state. The registry holds no hiring, funding, or tech type, so those rows emit no signal and no workflow trigger sees them. signals list and leads-today cover the four engagement types only.
MCP
Discover Signals and Feed tools with oxygen_capabilities_search, inspect the selected schema, then use oxygen_capabilities_read for read-only tools or oxygen_capabilities_call for tools that write or spend. Both work from core; ?toolset=signals optionally lists the pack directly. Its full contents are in the MCP reference.
Posts
Stored LinkedIn and X post performance, and the Publishing Community queue for public comments on owned posts.
Dashboards
The stitched touches-to-won funnel, which of its numbers are attributed and which are directional, how to read it from the web or the CLI, and the dashboards you build yourself in the CRM.