Skip to main content
Glama

Analytics Legends — SAP Analytics Intelligence

Search the public SAP AI & analytics contract radar

find_opportunities
Read-onlyIdempotent

Search every SAP contract and permanent-role posting Analytics Legends publishes to an ANONYMOUS visitor — the same population a human browses on /opportunities/, where each posting has its own prerendered page. It merges the platform's TWO public legs, which are near-disjoint (measured 2026-07-30: 1 row in common): (a) the PROMOTED feed (public.public_opportunities) — general SAP work (FI/CO, SD, EWM, MDG, BTP, ABAP), all German cities, dated (posted_at is populated on EVERY active row of that leg — an invariant held since 2026-07-31, not a snapshot). 🔴 THIS LEG CHANGED SHAPE ON 2026-08-28: until then it was fed by three keyless APIs and carried no contract_type, country_code, expires_at or rate at all; it was then loaded from the site radar and now declares contract_type and country_code on most of its rows, an expiry on most, and an advertised rate on a small minority. Do NOT assume a field is null on this leg — read the _meta counters on YOUR OWN response, which are computed at query time; (b) the SITE RADAR (/api/contracts-lean.json) — these carry country, category, seniority, posted_at, employment_type and, on most of them, expires_at; they are the analytics-specific ones (SAC Planning, Datasphere Technical Lead, Business Data Cloud). READ employment_type BEFORE CALLING THIS A CONTRACT MARKET: the radar is mostly PERMANENT roles, so an unfiltered page answers a freelance question with salaried jobs unless you filter. The argument of the same name does the filtering, and _meta.tranche_total_row_count on your own response is the live population — read the split from a filtered call, never from a figure quoted in this text. TWO DIFFERENT RATE FIELDS, AND THEY MEAN DIFFERENT THINGS. currency / daily_rate_min / daily_rate_max are the posting's OWN advertised rate and are almost always null — most listings publish no rate at all. rate_band is the platform's editorial benchmark for that posting's (seniority × product × region) cell. It is a SUBSCRIBER surface of the WEBSITE, not of this server: the radar file this server reads carries no band since 2026-08-29, so rate_band is null on EVERY row here, with or without a key (each response's _meta.rate_band_coverage counts it). The panel grid itself covers European cells only — DACH, FR/Benelux, UK/IE, Southern EU, Eastern EU, Nordics — so a posting in the Americas, APAC, Africa or the Middle East has no band at ANY tier: never promise one for it. It is rate_basis: "panel_inferred" — Eursap n=312 plus the Analytics Legends operator panel, permanent rows restated as a TJM equivalent at ~220 billable days a year — NOT a rate this employer offered. Quote it as a band with its basis, kind and source, never as the posting's rate, and never average bands across postings: many rows share one cell. WHAT IS GATED IS A FIELD, NOT A ROW: on most radar rows source_url is null and application_link reads "members_only" — the verified link to the original listing is the paid Consultant-tier deliverable. Everything else about the posting is public, and citation_url is that posting's own page on analyticslegends.ai. Quote it. Report _meta.tranche_row_count as the published public population, never as the size of the market.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
langNoReading language for the TITLE — 'EN' (default), 'FR' or 'DE'. This is a RENDERING choice, never a filter: it changes which string `title` carries, never which rows come back. Read `title_lang` on every row for the language actually served: it differs from what you asked for exactly when that translation does not exist (the live FR/DE coverage is counted on every response in `_meta.untranslated_leg`), and the verbatim is served instead, labelled with the language the harvest chain measured. `source_lang` always carries the language the ADVERTISER wrote in, translated or not. The promoted leg has no translated columns at all: its rows ignore this argument and say so with `title_lang: null` — see `_meta.untranslated_leg`.
limitNoMax rows (hard cap 50).
queryNoFree-text filter, case-insensitive. EVERY word must appear in the record (substring per word, any order), so a natural-language phrase narrows the answer instead of having to match verbatim.
cursorNoOpaque token from a previous response's `_meta.next_cursor`. Pass it back with the SAME filter arguments; `null` means the last page. Changing a filter refuses the cursor.
countryNoISO-3166-1 alpha-2 code, applied to both legs as a predicate on the row's own country_code. It NO LONGER selects the site-radar leg alone: the promoted feed carried country_code on almost none of its rows until 2026-08-28 and now carries it on most, so a country filter now returns both legs. A row still without one is dropped because it does not match, not because its leg was excluded by assumption. `_meta.match_count_by_leg` shows what each leg contributed on YOUR call — read the split there, never from a figure quoted in this text.
locationNoCity or place, matched case-insensitively as a substring of the posting's location. The promoted leg is all-German (Hamburg, Frankfurt am Main, Bremen, Munich, Cologne, Dortmund, Hanover, Landshut, Mannheim, Stuttgart); the site-radar leg is worldwide.
remote_modeNoRestrict to one work-location policy: `remote`, `hybrid` or `onsite`. READ THIS BEFORE ANSWERING A REMOTE QUESTION: a large share of the radar declares no policy at all (`_meta.remote_mode_undeclared` carries the live count — roughly half the radar when last measured, and a frozen pair written here drifted ~30% in two days), and an undeclared row is NOT an on-site row — it is a posting that does not say. Any value here therefore sets those rows aside rather than classifying them, exactly as the site's own filter does, and `_meta.remote_mode_undeclared` reports how many were set aside. The promoted leg carries its own `remote_mode` column and is filtered by the same predicate. Read `_meta.available_remote_modes` for the live spread before assuming a value exists.
employment_typeNoRestrict to one engagement type. THE RADAR IS MOSTLY PERMANENT, so a freelance or contract question answered off an unfiltered page is answered with salaried jobs. For the actual split, make the filtered call and read `_meta.tranche_total_row_count` — it is counted at query time. The promoted leg declared NO contract_type until 2026-08-28 and now declares one on most of its rows, so a value here no longer drops that leg wholesale — only the rows still silent. THOSE ROWS ARE NOT A FOURTH TYPE AND NOT PERMANENT ONES: `_meta.available_employment_types` counts only what declares, and `_meta.employment_type_undeclared` carries the rest, so the two together are the rows THIS call read (`_meta.employment_mix_rows_read` — a window, NOT the tranche: `_meta.tranche_total_row_count` is the population) and either alone is not even that. Read both before quoting a mix, and quote the window with it.

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
rowsYes
toolYes
_metaNo
_attributionYes
result_countYes

Schema Changelog

Changes observed during successful MCP inspections.

  1. Changed6 schema fields changed
    • changedInput schema / properties / lang / description
      Previous value: -"Reading language for the TITLE — 'EN' (default), 'FR' or 'DE'. This is a RENDERING choice, never a filter: it changes which string `title` carries, never which rows come back. Read `title_lang` on every row for the language actually served: it differs from what you asked for exactly when that translation does not exist (FR covers 1,492 of 1,588 site-radar rows, DE 1,373 — measured 2026-09-04), and the verbatim is served instead, labelled with the language the harvest chain measured. `source_lang` always carries the language the ADVERTISER wrote in, translated or not. The promoted leg has no translated columns at all: its rows ignore this argument and say so with `title_lang: null` — see `_meta.untranslated_leg`."New value: +"Reading language for the TITLE — 'EN' (default), 'FR' or 'DE'. This is a RENDERING choice, never a filter: it changes which string `title` carries, never which rows come back. Read `title_lang` on every row for the language actually served: it differs from what you asked for exactly when that translation does not exist (the live FR/DE coverage is counted on every response in `_meta.untranslated_leg`), and the verbatim is served instead, labelled with the language the harvest chain measured. `source_lang` always carries the language the ADVERTISER wrote in, translated or not. The promoted leg has no translated columns at all: its rows ignore this argument and say so with `title_lang: null` — see `_meta.untranslated_leg`."
    • addedOutput schema / properties / rows / items / properties / canonical_id
      Added value: +{
      +  "description": "Set when the site folded this posting into another one it judged identical: `citation_url` is then the page of THAT posting (`canonical_id`). When both are matched, only the canonical row is served and `_meta.duplicates_folded` counts the fold. Null otherwise.",
      +  "type": [
      +    "string",
      +    "null"
      +  ]
      +}
    • addedOutput schema / properties / rows / items / properties / salary_min / description
      Added value: +"The posting's own advertised salary, YEARLY (see `salary_period`). Null when none was published, and null when the published amount cannot be a yearly salary — `salary_withheld_reason` then says why."
    • addedOutput schema / properties / rows / items / properties / salary_period
      Added value: +{
      +  "description": "`year` when salary_min/salary_max are served. Null otherwise — never a guessed unit.",
      +  "type": [
      +    "string",
      +    "null"
      +  ]
      +}
    • addedOutput schema / properties / rows / items / properties / salary_withheld_reason
      Added value: +{
      +  "description": "Why a published salary was NOT served: `below_annual_floor` (the low end is below any yearly salary in that currency — a monthly or daily figure filed as a salary) or `board_estimate` (the job board's own min = max estimate, not the employer's). The amount is withheld, never converted. Null when nothing was withheld.",
      +  "type": [
      +    "string",
      +    "null"
      +  ]
      +}
    • changedOutput schema / properties / rows / items / properties / title_lang / description
      Previous value: -"ISO-639-1 language the SERVED `title` is written in. READ IT ON EVERY ROW: it is what you asked for in `lang` when that translation exists, and the advertiser's own language when it does not (FR covers 1,492 of 1,588 site-radar rows, DE 1,373 — measured 2026-09-04). Null when the harvest chain could not decide, and on every promoted-leg row, which carries no translated column at all. ⚠️ THIS FIELD CHANGED REFERENT ON 2026-09-04: until then it named the language the ADVERTISER wrote in — that fact now lives in `source_lang`. The two coincided while no translation was served and diverge from the first one that is."New value: +"ISO-639-1 language the SERVED `title` is written in. READ IT ON EVERY ROW: it is what you asked for in `lang` when that translation exists, and the advertiser's own language when it does not (the live FR/DE coverage is counted on every response in `_meta.untranslated_leg`). Null when the harvest chain could not decide — on some site-radar rows too, counted in `_meta.untranslated_radar_rows` — and on every promoted-leg row, which carries no translated column at all. ⚠️ THIS FIELD CHANGED REFERENT ON 2026-09-04: until then it named the language the ADVERTISER wrote in — that fact now lives in `source_lang`. The two coincided while no translation was served and diverge from the first one that is."
  2. Changed1 schema field changed
    • changedInput schema / properties / employment_type / description
      Previous value: -"Restrict to one engagement type. THE RADAR IS MOSTLY PERMANENT, so a freelance or contract question answered off an unfiltered page is answered with salaried jobs. For the actual split, make the filtered call and read `_meta.tranche_total_row_count` — it is counted at query time. The promoted leg declared NO contract_type until 2026-08-28 and now declares one on most of its rows, so a value here no longer drops that leg wholesale — only the rows still silent. THOSE ROWS ARE NOT A FOURTH TYPE AND NOT PERMANENT ONES: `_meta.available_employment_types` counts only what declares, and `_meta.employment_type_undeclared` carries the rest, so the two together are the population and either alone is not. Read both before quoting a mix."New value: +"Restrict to one engagement type. THE RADAR IS MOSTLY PERMANENT, so a freelance or contract question answered off an unfiltered page is answered with salaried jobs. For the actual split, make the filtered call and read `_meta.tranche_total_row_count` — it is counted at query time. The promoted leg declared NO contract_type until 2026-08-28 and now declares one on most of its rows, so a value here no longer drops that leg wholesale — only the rows still silent. THOSE ROWS ARE NOT A FOURTH TYPE AND NOT PERMANENT ONES: `_meta.available_employment_types` counts only what declares, and `_meta.employment_type_undeclared` carries the rest, so the two together are the rows THIS call read (`_meta.employment_mix_rows_read` — a window, NOT the tranche: `_meta.tranche_total_row_count` is the population) and either alone is not even that. Read both before quoting a mix, and quote the window with it."
  3. Changed3 schema fields changed
    • addedInput schema / properties / lang
      Added value: +{
      +  "description": "Reading language for the TITLE — 'EN' (default), 'FR' or 'DE'. This is a RENDERING choice, never a filter: it changes which string `title` carries, never which rows come back. Read `title_lang` on every row for the language actually served: it differs from what you asked for exactly when that translation does not exist (FR covers 1,492 of 1,588 site-radar rows, DE 1,373 — measured 2026-09-04), and the verbatim is served instead, labelled with the language the harvest chain measured. `source_lang` always carries the language the ADVERTISER wrote in, translated or not. The promoted leg has no translated columns at all: its rows ignore this argument and say so with `title_lang: null` — see `_meta.untranslated_leg`.",
      +  "pattern": "^[A-Za-z]{2}$",
      +  "type": "string"
      +}
    • addedOutput schema / properties / rows / items / properties / source_lang
      Added value: +{
      +  "description": "ISO-639-1 language the ADVERTISER wrote the title in, measured by the harvest chain and never changed by `lang` — 'en' on 1,566 rows, 'de' on 337 (2026-08-23). Null when the chain could not decide, and on the promoted leg. Compare it with `title_lang` to know whether the title you are quoting is the advertiser's own words or this platform's rendering of them: a harvested posting is a third party's text, and saying which of the two you are citing is the only honest way to quote it.",
      +  "type": [
      +    "string",
      +    "null"
      +  ]
      +}
    • changedOutput schema / properties / rows / items / properties / title_lang / description
      Previous value: -"ISO-639-1 language the TITLE is written in, as measured by the harvest chain — 'en' on 1,566 rows, 'de' on 337 (2026-08-23). Null when the chain could not decide. A harvested title is a third party's own words and is never translated, so this label is the only honest way to tell a reader the posting you are quoting is not in their language. Absent from the served payload until 2026-08-23: the projector's keep-list dropped it."New value: +"ISO-639-1 language the SERVED `title` is written in. READ IT ON EVERY ROW: it is what you asked for in `lang` when that translation exists, and the advertiser's own language when it does not (FR covers 1,492 of 1,588 site-radar rows, DE 1,373 — measured 2026-09-04). Null when the harvest chain could not decide, and on every promoted-leg row, which carries no translated column at all. ⚠️ THIS FIELD CHANGED REFERENT ON 2026-09-04: until then it named the language the ADVERTISER wrote in — that fact now lives in `source_lang`. The two coincided while no translation was served and diverge from the first one that is."
  4. Changed1 schema field changed
    • changedOutput schema / properties / rows / items / properties / rate_band / description
      Previous value: -"Editorial benchmark for this posting's (seniority × product × region) cell — NOT a rate the employer offered. `basis` and `kind` are inside the object on purpose, so no extraction can lift the numbers away from what they mean."New value: +"Editorial benchmark for this posting's (seniority × product × region) cell — NOT a rate the employer offered. `basis` and `kind` are inside the object on purpose, so no extraction can lift the numbers away from what they mean. 2026-08-29: this band is a SUBSCRIBER surface and is no longer carried by the public radar file this lane reads — expect it to be null here. The posting's OWN published rate, when it has one, stays in currency / daily_rate_min / daily_rate_max."
  5. Changed2 schema fields changed
    • changedInput schema / properties / country / description
      Previous value: -"ISO-3166-1 alpha-2 code, applied to both legs as a predicate on the row's own country_code. It effectively selects the SITE-RADAR leg: the promoted feed leaves country_code NULL on all but a handful of its active rows, so a country filter drops the rest of that leg because they do not match, not because the leg was excluded by assumption. `_meta.match_count_by_leg` shows what each leg contributed on YOUR call — read the split there, never from a figure quoted in this text."New value: +"ISO-3166-1 alpha-2 code, applied to both legs as a predicate on the row's own country_code. It NO LONGER selects the site-radar leg alone: the promoted feed carried country_code on almost none of its rows until 2026-08-28 and now carries it on most, so a country filter now returns both legs. A row still without one is dropped because it does not match, not because its leg was excluded by assumption. `_meta.match_count_by_leg` shows what each leg contributed on YOUR call — read the split there, never from a figure quoted in this text."
    • changedInput schema / properties / employment_type / description
      Previous value: -"Restrict to one engagement type. THE RADAR IS MOSTLY PERMANENT, so a freelance or contract question answered off an unfiltered page is answered with salaried jobs. For the actual split, make the filtered call and read `_meta.tranche_total_row_count` — it is counted at query time. The promoted leg stores contract_type NULL on every one of its active rows, so any value here drops that leg by predicate — `_meta.note` says so. THOSE ROWS ARE NOT A FOURTH TYPE AND NOT PERMANENT ONES: `_meta.available_employment_types` counts only what declares, and `_meta.employment_type_undeclared` carries the rest, so the two together are the population and either alone is not. Read both before quoting a mix."New value: +"Restrict to one engagement type. THE RADAR IS MOSTLY PERMANENT, so a freelance or contract question answered off an unfiltered page is answered with salaried jobs. For the actual split, make the filtered call and read `_meta.tranche_total_row_count` — it is counted at query time. The promoted leg declared NO contract_type until 2026-08-28 and now declares one on most of its rows, so a value here no longer drops that leg wholesale — only the rows still silent. THOSE ROWS ARE NOT A FOURTH TYPE AND NOT PERMANENT ONES: `_meta.available_employment_types` counts only what declares, and `_meta.employment_type_undeclared` carries the rest, so the two together are the population and either alone is not. Read both before quoting a mix."
  6. Changed1 schema field changed
    • changedInput schema / properties / employment_type / description
      Previous value: -"Restrict to one engagement type. THE RADAR IS MOSTLY PERMANENT, so a freelance or contract question answered off an unfiltered page is answered with salaried jobs. For the actual split, make the filtered call and read `_meta.tranche_total_row_count` — it is counted at query time. The promoted leg stores contract_type NULL on every one of its active rows, so any value here drops that leg by predicate — `_meta.note` says so."New value: +"Restrict to one engagement type. THE RADAR IS MOSTLY PERMANENT, so a freelance or contract question answered off an unfiltered page is answered with salaried jobs. For the actual split, make the filtered call and read `_meta.tranche_total_row_count` — it is counted at query time. The promoted leg stores contract_type NULL on every one of its active rows, so any value here drops that leg by predicate — `_meta.note` says so. THOSE ROWS ARE NOT A FOURTH TYPE AND NOT PERMANENT ONES: `_meta.available_employment_types` counts only what declares, and `_meta.employment_type_undeclared` carries the rest, so the two together are the population and either alone is not. Read both before quoting a mix."
  7. Changed1 schema field changed
    • changedInput schema / properties / query / description
      Previous value: -"Free-text filter, matched case-insensitively."New value: +"Free-text filter, case-insensitive. EVERY word must appear in the record (substring per word, any order), so a natural-language phrase narrows the answer instead of having to match verbatim."
  8. Changed1 schema field changed
    • addedOutput schema / properties / rows / items / properties / title_lang
      Added value: +{
      +  "description": "ISO-639-1 language the TITLE is written in, as measured by the harvest chain — 'en' on 1,566 rows, 'de' on 337 (2026-08-23). Null when the chain could not decide. A harvested title is a third party's own words and is never translated, so this label is the only honest way to tell a reader the posting you are quoting is not in their language. Absent from the served payload until 2026-08-23: the projector's keep-list dropped it.",
      +  "type": [
      +    "string",
      +    "null"
      +  ]
      +}
  9. Changed1 schema field changed
    • changedInput schema / properties / cursor / description
      Previous value: -"Opaque token from a previous response's `_meta.next_cursor`. Pass it back with the SAME filter arguments to read the next page; a null `next_cursor` means you have reached the end. It is bound to those filters and refused if they change — a cursor names a POSITION in one ordering, and applying it to another query would start the page in the wrong place."New value: +"Opaque token from a previous response's `_meta.next_cursor`. Pass it back with the SAME filter arguments; `null` means the last page. Changing a filter refuses the cursor."
  10. Changed16 schema fields changed
    • changedInput schema / properties / country / description
      Previous value: -"ISO-3166-1 alpha-2 code, applied to both legs as a predicate on the row's own country_code. It effectively selects the SITE-RADAR leg: the promoted feed stores country_code on 2 of its 109 active rows (measured 2026-08-10), so a country filter drops the rest of that leg because they do not match, not because the leg was excluded by assumption. `_meta.match_count_by_leg` shows what each leg contributed."New value: +"ISO-3166-1 alpha-2 code, applied to both legs as a predicate on the row's own country_code. It effectively selects the SITE-RADAR leg: the promoted feed leaves country_code NULL on all but a handful of its active rows, so a country filter drops the rest of that leg because they do not match, not because the leg was excluded by assumption. `_meta.match_count_by_leg` shows what each leg contributed on YOUR call — read the split there, never from a figure quoted in this text."
    • changedInput schema / properties / remote_mode / description
      Previous value: -"Restrict to one work-location policy: `remote`, `hybrid` or `onsite`. READ THIS BEFORE ANSWERING A REMOTE QUESTION: the radar declares no policy at all on 1,409 of its 2,661 active rows (measured 2026-08-14), and an undeclared row is NOT an on-site row — it is a posting that does not say. Any value here therefore sets those rows aside rather than classifying them, exactly as the site's own filter does, and `_meta.remote_mode_undeclared` reports how many were set aside. The promoted leg carries its own `remote_mode` column and is filtered by the same predicate. Read `_meta.available_remote_modes` for the live spread before assuming a value exists."New value: +"Restrict to one work-location policy: `remote`, `hybrid` or `onsite`. READ THIS BEFORE ANSWERING A REMOTE QUESTION: a large share of the radar declares no policy at all (`_meta.remote_mode_undeclared` carries the live count — roughly half the radar when last measured, and a frozen pair written here drifted ~30% in two days), and an undeclared row is NOT an on-site row — it is a posting that does not say. Any value here therefore sets those rows aside rather than classifying them, exactly as the site's own filter does, and `_meta.remote_mode_undeclared` reports how many were set aside. The promoted leg carries its own `remote_mode` column and is filtered by the same predicate. Read `_meta.available_remote_modes` for the live spread before assuming a value exists."
    • addedOutput schema / properties / rows / items / properties / application_link
      Added value: +{
      +  "description": "`public` (source_url is served) or `members_only` (the link to the original listing is the paid Consultant-tier deliverable; the row and its facts stay public).",
      +  "type": [
      +    "string",
      +    "null"
      +  ]
      +}
    • addedOutput schema / properties / rows / items / properties / category
      Added value: +{
      +  "type": [
      +    "string",
      +    "null"
      +  ]
      +}
    • addedOutput schema / properties / rows / items / properties / currency
      Added value: +{
      +  "type": [
      +    "string",
      +    "null"
      +  ]
      +}
    • addedOutput schema / properties / rows / items / properties / daily_rate_max
      Added value: +{
      +  "type": [
      +    "number",
      +    "null"
      +  ]
      +}
    • addedOutput schema / properties / rows / items / properties / daily_rate_min
      Added value: +{
      +  "description": "Only when the source declares a DAILY rate period — never converted from other periods.",
      +  "type": [
      +    "number",
      +    "null"
      +  ]
      +}
    • addedOutput schema / properties / rows / items / properties / description
      Added value: +{
      +  "description": "Posting text, HTML stripped, clamped. Null on the radar leg.",
      +  "type": [
      +    "string",
      +    "null"
      +  ]
      +}
    • addedOutput schema / properties / rows / items / properties / duration_months
      Added value: +{
      +  "type": [
      +    "number",
      +    "null"
      +  ]
      +}
    • addedOutput schema / properties / rows / items / properties / id
      Added value: +{
      +  "description": "The posting's identity — stable across calls, key for dedup.",
      +  "type": [
      +    "string",
      +    "null"
      +  ]
      +}
    • addedOutput schema / properties / rows / items / properties / leg
      Added value: +{
      +  "description": "`promoted` or `site_radar` — which of the two merged public legs served this row.",
      +  "type": [
      +    "string",
      +    "null"
      +  ]
      +}
    • addedOutput schema / properties / rows / items / properties / rate_period
      Added value: +{
      +  "type": [
      +    "string",
      +    "null"
      +  ]
      +}
    • addedOutput schema / properties / rows / items / properties / salary_max
      Added value: +{
      +  "type": [
      +    "number",
      +    "null"
      +  ]
      +}
    • addedOutput schema / properties / rows / items / properties / salary_min
      Added value: +{
      +  "type": [
      +    "number",
      +    "null"
      +  ]
      +}
    • addedOutput schema / properties / rows / items / properties / skills
      Added value: +{
      +  "description": "Skills the posting names. Null when it names none.",
      +  "type": [
      +    "array",
      +    "null"
      +  ]
      +}
    • addedOutput schema / properties / rows / items / properties / source
      Added value: +{
      +  "type": [
      +    "string",
      +    "null"
      +  ]
      +}
  11. Changed1 schema field changed
    • changedInput schema / properties / employment_type / description
      Previous value: -"Restrict to one engagement type. THE RADAR IS MOSTLY PERMANENT (2,172 permanent, 176 freelance, 33 contract of 2,381 active rows, measured 2026-08-10), so a freelance or contract question answered off an unfiltered page is answered with salaried jobs. The promoted leg stores contract_type NULL on all 109 of its active rows, so any value here drops that leg by predicate — `_meta.note` says so."New value: +"Restrict to one engagement type. THE RADAR IS MOSTLY PERMANENT, so a freelance or contract question answered off an unfiltered page is answered with salaried jobs. For the actual split, make the filtered call and read `_meta.tranche_total_row_count` — it is counted at query time. The promoted leg stores contract_type NULL on every one of its active rows, so any value here drops that leg by predicate — `_meta.note` says so."
  12. Changed5 schema fields changed
    • addedInput schema / properties / remote_mode
      Added value: +{
      +  "description": "Restrict to one work-location policy: `remote`, `hybrid` or `onsite`. READ THIS BEFORE ANSWERING A REMOTE QUESTION: the radar declares no policy at all on 1,409 of its 2,661 active rows (measured 2026-08-14), and an undeclared row is NOT an on-site row — it is a posting that does not say. Any value here therefore sets those rows aside rather than classifying them, exactly as the site's own filter does, and `_meta.remote_mode_undeclared` reports how many were set aside. The promoted leg carries its own `remote_mode` column and is filtered by the same predicate. Read `_meta.available_remote_modes` for the live spread before assuming a value exists.",
      +  "type": "string"
      +}
    • addedOutput schema / properties / rows / items / properties / country_code
      Added value: +{
      +  "description": "ISO-3166-1 alpha-2 of the posting's own country. Null on almost all of the promoted leg.",
      +  "type": [
      +    "string",
      +    "null"
      +  ]
      +}
    • addedOutput schema / properties / rows / items / properties / posted_at
      Added value: +{
      +  "type": [
      +    "string",
      +    "null"
      +  ]
      +}
    • addedOutput schema / properties / rows / items / properties / remote_mode
      Added value: +{
      +  "description": "The posting's declared work-location policy (`remote` / `hybrid` / `onsite`). NULL means the posting does not say — never read it as on-site.",
      +  "type": [
      +    "string",
      +    "null"
      +  ]
      +}
    • addedOutput schema / properties / rows / items / properties / seniority
      Added value: +{
      +  "type": [
      +    "string",
      +    "null"
      +  ]
      +}
  13. Changed5 schema fields changed
    • changedInput schema / properties / country / description
      Previous value: -"ISO-3166-1 alpha-2 code. It can only match the SITE-RADAR leg: the promoted leg stores country_code NULL on every row, so filtering by country silently drops it rather than returning nothing. `_meta.note` says so on any country-filtered call."New value: +"ISO-3166-1 alpha-2 code, applied to both legs as a predicate on the row's own country_code. It effectively selects the SITE-RADAR leg: the promoted feed stores country_code on 2 of its 109 active rows (measured 2026-08-10), so a country filter drops the rest of that leg because they do not match, not because the leg was excluded by assumption. `_meta.match_count_by_leg` shows what each leg contributed."
    • addedInput schema / properties / employment_type
      Added value: +{
      +  "description": "Restrict to one engagement type. THE RADAR IS MOSTLY PERMANENT (2,172 permanent, 176 freelance, 33 contract of 2,381 active rows, measured 2026-08-10), so a freelance or contract question answered off an unfiltered page is answered with salaried jobs. The promoted leg stores contract_type NULL on all 109 of its active rows, so any value here drops that leg by predicate — `_meta.note` says so.",
      +  "enum": [
      +    "freelance",
      +    "contract",
      +    "permanent"
      +  ],
      +  "type": "string"
      +}
    • addedOutput schema / properties / rows / items / properties / employment_type
      Added value: +{
      +  "type": [
      +    "string",
      +    "null"
      +  ]
      +}
    • addedOutput schema / properties / rows / items / properties / expires_at
      Added value: +{
      +  "type": [
      +    "string",
      +    "null"
      +  ]
      +}
    • addedOutput schema / properties / rows / items / properties / rate_band
      Added value: +{
      +  "description": "Editorial benchmark for this posting's (seniority × product × region) cell — NOT a rate the employer offered. `basis` and `kind` are inside the object on purpose, so no extraction can lift the numbers away from what they mean.",
      +  "type": [
      +    "object",
      +    "null"
      +  ]
      +}
  14. Changed1 schema field changed
    • addedInput schema / properties / cursor
      Added value: +{
      +  "description": "Opaque token from a previous response's `_meta.next_cursor`. Pass it back with the SAME filter arguments to read the next page; a null `next_cursor` means you have reached the end. It is bound to those filters and refused if they change — a cursor names a POSITION in one ordering, and applying it to another query would start the page in the wrong place.",
      +  "maxLength": 512,
      +  "type": "string"
      +}
  15. Changed5 schema fields changed
    • removedOutput schema / properties / rows / items / properties / citation_note
      Removed value: -{
      -  "type": "string"
      -}
    • changedOutput schema / properties / rows / items / properties / firm_name / type
      Previous value: -"string"New value: +[
      +  "string",
      +  "null"
      +]
    • changedOutput schema / properties / rows / items / properties / location / type
      Previous value: -"string"New value: +[
      +  "string",
      +  "null"
      +]
    • changedOutput schema / properties / rows / items / properties / source_url / type
      Previous value: -"string"New value: +[
      +  "string",
      +  "null"
      +]
    • changedOutput schema / properties / rows / items / properties / title / type
      Previous value: -"string"New value: +[
      +  "string",
      +  "null"
      +]
  16. Changed3 schema fields changed
    • addedOutput schema / properties / rows / items / properties / citation_note
      Added value: +{
      +  "type": "string"
      +}
    • addedOutput schema / properties / rows / items / properties / citation_scope
      Added value: +{
      +  "enum": [
      +    "record",
      +    "section_hub"
      +  ],
      +  "type": "string"
      +}
    • addedOutput schema / properties / rows / items / properties / citation_url
      Added value: +{
      +  "type": "string"
      +}
  17. Changed2 schema fields changed
    • addedInput schema / properties / country
      Added value: +{
      +  "description": "ISO-3166-1 alpha-2 code. It can only match the SITE-RADAR leg: the promoted leg stores country_code NULL on every row, so filtering by country silently drops it rather than returning nothing. `_meta.note` says so on any country-filtered call.",
      +  "pattern": "^[A-Za-z]{2}$",
      +  "type": "string"
      +}
    • changedInput schema / properties / location / description
      Previous value: -"City, matched case-insensitively as a substring of the posting's location. Measured 2026-07-30, the 19 active rows carry 10 cities, all German: Hamburg (6), Frankfurt am Main (3), Bremen (2), Munich (2), Cologne, Dortmund, Hanover, Landshut, Mannheim, Stuttgart. There is NO country filter: country_code is NULL on every row, so the radar cannot be sliced by country — that is a gap in this corpus, not in the market."New value: +"City or place, matched case-insensitively as a substring of the posting's location. The promoted leg is all-German (Hamburg, Frankfurt am Main, Bremen, Munich, Cologne, Dortmund, Hanover, Landshut, Mannheim, Stuttgart); the site-radar leg is worldwide."
  18. First observed

TDQS

A4.2/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already cover the safety profile (readOnly, idempotent, non-destructive, closed-world), and the description still adds substantial undisclosed behavior: most radar rows have `source_url: null` and `application_link: "members_only"` (the gated Consultant-tier field), `rate_band` is null on every row with or without a key, cursors are refused when filters change, and the promoted leg's schema shape changed on a dated cutover. That is exactly the kind of context annotations cannot convey.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness2/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The purpose is front-loaded, but the body runs to roughly a thousand words with repeated injunctions ("read the `_meta` on your own response", "never from a figure quoted in this text") and historical churn narratives dated 2026-07-30/08-28/08-29 that do not help an agent decide or invoke. Much of it duplicates the already-verbose parameter descriptions.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For an eight-parameter, two-leg search tool with gated fields and editorial rate bands, the definition covers population, filtering, gating, null-field behavior and pagination thoroughly. With an output schema present, return values need not be explained, so nothing material is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100% and the schema itself is already extremely detailed, so the baseline is 3. The description mostly restates schema semantics (lang as a rendering choice, country applying to both legs, remote_mode setting undeclared rows aside) rather than adding parameter-level meaning the schema lacks.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The opening states a specific verb and resource — searching every SAP contract and permanent-role posting that Analytics Legends publishes publicly — and scopes it to the anonymous visitor population behind /opportunities/. It is clearly distinguishable from siblings like search_firms, get_day_rate_benchmark or search_news, which address different corpora.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It gives strong conditional guidance: read `employment_type` before treating this as a contract market, use the argument to filter, expect `rate_band` to be null everywhere, and never quote bands across postings. It does not, however, name a sibling tool as the alternative when this tool is the wrong one, so routing is implied rather than explicit.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Try in Browser

Glama MCP Gateway

Add one secure layer between your agents and this server.