Skip to main content
A discovery watch notifies you about one kind of record: companies, people, or jobs. Its filters are not limited to that one kind. You can write a company watch that filters on the roles a company is hiring for, a job watch that filters on the company behind the posting, or a person watch that filters on their employer’s technology stack. There is no new syntax and no second filter list. You write one flat set of conditions, mixing fields from any dataset, and the watch works out where each field lives.
That is a company watch. technographics.technologies.name is a company field and job_details.title is a job field, so the watch delivers companies running Snowflake that are hiring a data engineer. Neither condition could be expressed in a single-dataset watch: technology stack is not carried on job records, and open roles are not carried on company records.

Why this exists

A job record already carries some of its company’s attributes, so a job watch can filter on funding stage, headcount, industry, and head office without leaving the job dataset. Nothing marks those fields as special from the outside, which made the boundary invisible: filtering a job watch on funding stage worked, and filtering the same watch on technology stack failed. Cross-dataset filters put every filter field on all three search endpoints within reach of any discovery watch.

What you can combine

Every pairing works, and a single watch can pull from all three datasets at once.
People join through their current employer only. A filter on a past employer still selects the right people, but the company it points at is where they work now. “Companies that hired away from Acme” means companies that currently employ someone whose earlier experience was at Acme.

How a run works

1

Field names decide the dataset

Field names are unique across the three datasets, so the name alone identifies where a condition belongs. headcount.total is a company field, job_details.title a job field, experience.employment_details.current.title a person field. Every name is listed in the Company Search, Person Search, and Job Search references.A handful of names appear in more than one vocabulary. technographics.technologies.name, technographics.technologies.category, and technographics.technologies.super_category are on both company and job records, and indexed_at and updated_at are on all three. Your watch’s own dataset always wins those, so a job watch filtering on technographics.technologies.name reads the job record’s own copy rather than querying the company dataset.
2

We query the other dataset first

Conditions from another dataset run against that dataset and come back as a set of company IDs. We fetch the IDs and nothing else.
3

Those IDs narrow your watch

The IDs go back into the filter tree in the position the conditions occupied, as a membership condition. Your watch then runs as normal.
Delivery, deduplication, per-run caps, and credits are unchanged. A cross-dataset watch behaves like any other discovery watch, and its payload is the same shape.

Grouping

Conditions from the same dataset sitting side by side in a group are answered together, as one question. In this company watch:
job_details.title and location.country are both job fields, so the watch asks for one posting that is an engineering role and is in the United States. It does not return a company with an engineering role in India and a separate sales role in the United States. Writing the two job conditions inside their own and group means the same thing, so you can group them for readability without changing the result. Under or the two forms also agree: a posting matching A, or a posting matching B, is a posting matching A or B.

When a cross-dataset condition matches nothing

An empty result counts as false and travels up through your groups. Under and, the whole branch is false and the run delivers nothing. Under or, that branch drops out and the rest of the watch still runs.
A condition that matches nothing because of a bad value is accepted at creation and then quietly never fires. {"field": "job_details.employment_type", "type": "=", "value": "Full-time"} matches zero jobs, and a watch built on it stays silent forever. Run the condition through Job Search, Company Search, or Person Search first and confirm total_count is not zero.

Recipes

Every one of these is verified against the live API. Swap the notification channel for your own.
Post each one to the create endpoint for the dataset you want delivered. The first, third, and fourth tabs are company, company, and person watches, so they go to /watch/company/search and /watch/person/search. The second is a job watch and goes to /watch/job/search.

Three datasets in one watch

A company watch running Snowflake, hiring data engineers, and employing someone who used to work at Databricks:

“First time” watches

Set up a watch for companies that employ a machine learning engineer. Its free baseline run records every company that already has one and delivers almost nothing. After that, a company can only appear by joining the set for the first time. That gives you “hired their first machine learning engineer” with no extra configuration, and the same shape gives you first hire in a country, first sales hire, or first posting of any kind. Two caveats. It means first as far as our data reaches, so a coverage gap or an edited job title will both set it off. And it fires once: a company that hires an ML engineer, loses them, and hires another stays quiet the second time.

Records from the other dataset

The payload carries your watch’s own records and nothing else. A company watch tells you Acme matched. It does not tell you which posting or which person put Acme there, because only IDs cross between datasets, never records. That is also why you are not charged for the dataset you filtered on. To get those records, call that dataset’s own search or enrich API with the ID from the delivered record and the same conditions you filtered on. A company watch on Snowflake and job_details.title contains data engineer delivers company 8294878. This gets the posting that put it in the feed:
Repeat the job conditions in that call. Without them you get every open role at the company, not the ones the watch matched on. Going the other way, a delivered job record carries the company ID at company.basic_info.crustdata_company_id, and a delivered person record carries it as crustdata_company_id on each current employer. These are ordinary search and enrich calls, billed at their normal rate. See Pricing.

What is not supported

Negation across datasets

We reject the operators that negate a single record when they sit on a cross-dataset condition: !=, not_in, (!), not_contains, and geo_exclude.
On a company watch that reads like “companies not hiring engineers”, but it means something else. Each job record is judged on its own, so a company posting both a Software Engineer role and a Salesperson role matches on the salesperson row and gets delivered. Most companies of any size are hiring a mix of roles, so nearly all of them would slip through. The API rejects it with 400:
These operators are still fine on your watch’s own dataset. basic_info.year_founded != 2020 on a company watch is accepted, because there it means what it says.

Counting

You can ask whether at least one matching record exists. You cannot ask for a number. “Five or more open engineering roles”, “doubled their postings this quarter”, and “three or more people left” are all out of reach.

Growth on the other dataset

“Companies whose engineering headcount grew” works, because that figure sits on the company record. “Companies whose engineering postings grew” does not, because job records carry no growth figures.

Limits

We check both when you create the watch, so you find out immediately rather than on the first run. The first is the baseline limit. The baseline run delivers a sample of 5, but it reads the whole match set to record what already exists, and that read is what the 500,000 bounds. It is not a ceiling on the watch’s lifetime: later runs deliver whatever newly matches, bounded by config.max_results_per_run. A cross-dataset condition under and narrows the set before the baseline limit is measured, so a watch can pair a very broad condition on its own dataset with a narrow one from another. This is accepted even though headcount.total > 10 alone matches over four million companies:
Under or there is no narrowing, so the same pair is rejected:
A cross-dataset condition covering more than a million companies is rejected on its own terms, whatever the rest of the watch looks like. A job watch filtering on basic_info.year_founded > 2020 reaches roughly four million companies, and the 400 opens Your company filters cover more than 1,000,000 companies. before suggesting you add a location, a date, or a more specific title. The full envelope is in the Watch API reference, under Create a discovery watch. If we cannot reach the other dataset at creation time, the watch is not created and you can retry:

Timing and deduplication

A single-dataset watch narrows each run to records reindexed since the previous run. That shortcut does not hold once another dataset is involved: a company posts a job on Tuesday, the job record is new, and the company record is untouched. A cross-dataset watch re-evaluates the full current match set on every run instead. What stops a record arriving twice is the record of what has already been sent, not the time window. Each match is delivered once, ever, for a given watch. If it drops out of the set and comes back later, the watch stays quiet.

Credits

Credits work exactly as they do on a single-dataset watch. You pay for records delivered to you, at your watch’s own rate: 2 credits per company, 0.5 per person, 0.5 per job. Reading another dataset to narrow your filters is free, and the baseline run is free. See Pricing for the full table.

Company discovery watcher

Deliver companies that newly match a filter.

Person discovery watcher

Deliver people that newly match a filter.

Job watcher

Deliver jobs that newly match a filter.

Pricing

Credit costs across all Crustdata endpoints.