People

Find recent company moves

Required permissions: people:read.

Stateless search over our people data. Send source (the company left), destination (the company joined), or both. Or search without a company by title, the new role (same terms as /v1/find filters.title), narrowed by destinationFilters (hq.countries, headcount of the company joined); those two go only with a title search. Inclusive startedSince and exclusive startedBefore (the month the new role started, month precision) narrows any of these searches, with or without a company. One of source, destination or title is required. Returns moves observed in the last 90 days and writes nothing. One row per person per destination — we hold a row per source record, and those are collapsed; a title search returns one row per person. limit sets the page size: 25 by default, 100 at most, capped rather than rejected. hasMore says more matches may remain behind this page; send nextCursor back as cursor, verbatim and with the same search, to walk them. The walk resumes where the page stopped, so a later page neither repeats nor skips what an earlier one returned. hasMore reads the row count before duplicate records are collapsed, so a page can come back short and still be true — follow hasMore and nextCursor, never the row count. A source-only search may read a fixed sample rather than an ordered walk and cannot be paged; indexed source-only searches can page. Sending cursor on a sampled source-only search is rejected, nextCursor is null, and hasMore: true there means narrow the search. Report truncation for the requested period. Each row carries detectedAt (when we saw the move), startedAt (when the role began, to the month, often null) and startedAtPrecision (month or year). detectedSince filters detection recency: it keeps only moves with detectedAt >= the value you send — the DETECTION date, not the start date; startedSince filters the start, and both can be sent. It only narrows the 90-day window, starting at midnight UTC 90 days ago: an earlier value is clamped to that start and a future value matches nothing. Destination searches already return the newest moves first: narrowing the window does not recover omitted matches; it returns the same rows or a subset. This is a recency filter, not pagination. seniority is the second filter: it keeps only moves INTO a role at that level or above, read from the title of the role they joined — not the one they left, and not a title list you supply. The four seniority levels widen in order: cxo (the C-suite), vp (adds VP, SVP, EVP, Vice President and President), director (adds Director), head (adds Partner, Principal and Head of). With destination, it WIDENS the search rather than trimming the page — the search reads further through recent moves looking for matches instead of filtering the slice it would have read without it — and that wider read is still bounded, so a short or empty answer does not prove nobody senior joined. Source-only searches that report sampled: true filter the same fixed sample; they do not read further. Title matching is heuristic and excludes assistant/support roles. sampled: true means a source-only search read a small fixed sample of recent moves; an empty answer does not mean nobody left, and retrying gives the same answer while the underlying data is unchanged. A source-only search with sampled: false walked the newest moves out of that company instead of sampling them. With destination, sampled is false: the search reads a bounded slice of the newest moves into that company before filtering by source. hasMore: false does not prove completeness with either value of sampled. A title search reads the title index up to a cap and says sampled: true when the title is held by more people than that; each page reads a bounded number of moves, so it can come back short, even empty, with hasMore: true — follow the cursor. A company search with startedSince or startedBefore checks the start after reading each move, so it also reads a bounded number per page and pages the same way. Every row is a move between two companies; promotions and same-company title changes never appear here. For promotions, and for checks you repeat on a company over time, use the workspace job_change signal (create_signal, then run_discovery). That signal is not a departure search and does not provide a complete list of who left. A workspace can run only a few job-change searches at once. A search sent while that many are running comes back as 503 data_search_busy before anything is read: wait details.retryAfterSeconds, send it again, and send fewer at the same time.

post/v1/job-changes/search

Request

  • Base URL: https://api.trayo.ai
  • URL: https://api.trayo.ai/v1/job-changes/search
  • Auth: HTTP bearer

Request body

detectedSincestring

Only moves with detectedAt >= this value. It filters detectedAt — when Trayo first observed the new role — and NOT startedAt, the date the role began; the two can be months apart. To filter on startedAt, send startedSince; both can be sent, and both apply. The 90-day window starts at midnight UTC 90 days ago; detectedSince can only narrow it. An earlier value is clamped to that start, and a future value matches nothing. Destination searches already return the newest moves first: narrowing the window does not recover omitted matches; it returns the same rows or a subset. This is a recency filter, not pagination. Report hasMore truncation for the requested period. Send a plain date — 2026-06-07, read as midnight UTC — or a full ISO-8601 timestamp — 2026-06-07T14:30:00Z — when you need the time of day. Relative words are not accepted: yesterday, last-quarter, 7d and anything else that has to be resolved against "now" is rejected with a 400 (invalid_date). Compute the date yourself and send it.

seniority'cxo' | 'vp' | 'director' | 'head'

Keep only moves INTO a role at this level or above. It reads the title of the role they joined — not the one they left, and not a title list you supply. The four seniority levels widen in order: cxo is the C-suite (CEO, CFO, CTO, COO, CMO, CIO, CISO, CRO, CPO and other Chief … Officer titles); vp adds every vice-president form (VP, SVP, EVP, Vice President) and President; director adds Director; head adds Partner, Principal and Head of. With destination, sending it WIDENS the search rather than trimming the page: the search reads further through recent moves looking for matches, instead of filtering the same slice it would have read without it. That wider read is still bounded, so a short or empty answer does not prove nobody senior joined, and hasMore: false still does not prove completeness. Source-only searches that report sampled: true filter the same fixed sample; they do not read further. Matching is heuristic and excludes assistant/support roles. Titles are matched on their words, so a company that names its leaders unusually can come back empty at a level where people do hold the job.

limitinteger

How many moves to return in this page. Defaults to 25. Values above 100 are capped at 100, not rejected — the same ceiling the other paginating routes in this API use.

cursorstring

The nextCursor from the previous page, sent back verbatim with the SAME search. Omit it for the first page. It resumes the walk where the last page stopped, so a later page neither repeats nor skips what an earlier one returned. It is opaque: do not build or read one. Destination and title searches accept cursors; an indexed source-only search does too. A source-only search may read a fixed sample rather than an ordered walk: when sampled is true, it cannot resume and sending a cursor is rejected. A title search pages the same way a destination search does. Changing the search between pages invalidates the cursor.

startedSincestring

Keeps moves whose new role STARTED in this month or later, read from startedAt. Works with title, source or destination, and with detectedSince and seniority: every filter sent applies. Month precision, because Trayo holds the start month, never the day: 2026-08 and 2026-08-25 both mean "started in August 2026 or later", so a "last 30 days" question gets the whole starting month back — check startedAt before calling a row inside a day-level window. Roles in the cutoff month cannot be confirmed inside a day-level last-30-days window. A role recorded with only a start year, or with no start date, does not match. Moves are still read from the 90-day detection window, so a month before that window finds only roles detected inside it. The start is checked after each move is read, so with a company a page reads a bounded number of recent moves and can come back short, even empty, with hasMore: true: follow nextCursor. A sampled source-only search cannot be paged, so hasMore: true there means narrow it.

startedBeforestring

Exclusive upper bound on the start month, with source, destination or title. 2026-10 keeps roles started before October 2026. A full date uses its month. Combine with inclusive startedSince for adjacent, non-overlapping month slices. Year-only and missing start dates do not match; the 90-day detection window still applies.

Response

hasMoreboolean required

The page or its bounded scan filled, so more matches may remain. Send nextCursor back as cursor, with the same search, to get them. It reads the raw row count BEFORE duplicate records are collapsed, so a page can come back short — fewer rows than limit — and still be true. That is deliberate: it reports truncation rather than hiding it. Follow hasMore and nextCursor, never the row count. A sampled source-only search cannot be paged, so true there means narrow the search instead. A title search, and a company search with startedSince or startedBefore, and any indexed source-only search, reads a bounded number of moves per page, so a page can come back short, even empty, and still be true: follow the cursor. hasMore: false does not prove completeness, even when sampled is false: every search reads a bounded slice of recent moves before filtering, so matching moves can exist outside that slice.

nextCursorstring nullable required

Send this nextCursor back as cursor, verbatim and with the SAME search, for the next page. Null when there is no next page. The walk resumes where this page stopped, so a later page neither repeats nor skips what an earlier one returned, even when new moves are detected while you page. Null on a sampled source-only search, which reads a fixed sample and cannot be paged. A short page can still carry one, so follow the cursor rather than counting rows.

sampledboolean required

Whether the search used a sample. true: only source was sent and the search read a small fixed sample of recent moves and kept the ones that left that company. An empty answer does not mean nobody left; retrying gives the same answer while the underlying data is unchanged. false with only source: the search walked the newest moves out of that company instead of sampling them. false: destination was sent, with or without source; the search read a bounded slice of the newest moves into that company before filtering by source. hasMore: false does not prove completeness with either value of sampled; matching moves may be outside the slice. With title, true means more people hold the words of that title than the search reads, so it looked at part of them: narrow the title or add all terms rather than treating the answer as complete (exclude, destinationFilters and startedSince filter what was read and do not widen it). Promotions and same-company title changes never appear here. For promotions, and for checks you repeat on a company over time, use the workspace job_change signal (create_signal, then run_discovery). That signal is not a departure search and does not provide a complete list of who left.

Changes

No recorded changes to this operation across all 1 revision of this API.