Google Reviews Response Backlog Builder avatar

Google Reviews Response Backlog Builder

Pricing

from $5.60 / 1,000 review assessments

Go to Apify Store
Google Reviews Response Backlog Builder

Google Reviews Response Backlog Builder

Build a response queue from Google Maps review datasets. Separate answered, unanswered and unknown records, show rating and age evidence, and prioritize each useful review assessment.

Pricing

from $5.60 / 1,000 review assessments

Rating

0.0

(0)

Developer

Tim Zinin

Tim Zinin

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

3 days ago

Last modified

Share

Google Reviews Response Backlog Builder — turn a reviews batch into a ranked response queue

Feed it an existing Google Maps reviews dataset and get back one review assessment per unique review — answered/unanswered/unknown status, a disclosed urgency score, and bounded keyword-bucket signals — never an AI reading of the review or an automated reply.

Google Reviews Response Backlog Builder — hero diagram

This Actor does not scrape Google Maps. It reads a reviews batch you already have — most naturally from Google Maps Reviews Scraper and/or Google Maps Places Scraper — and turns it into one response-backlog record per unique reviewId: whether the owner has answered, how urgent an unanswered review is, and which of eight fixed issue-keyword buckets its text matched. It is a triage tool for teams who already have a reviews batch — not a review reader, not a reply sender, and not a sentiment-accuracy engine.

What you get

For each distinct review found across your source batch, you get one assessment record containing:

  • entityId/reviewId — the canonical, trimmed review ID (identical on both fields).
  • status/responseStatus — answered, unanswered, or unknown. unknown covers a missing/wrong-typed responseFromOwnerText field or a detected responseDateTextConflict (see below) — never a confident guess.
  • urgency/urgencyScore — resolved/urgent/moderate/low/unknown, from a disclosed additive rule (base points for unanswered, a star-rating component, an age component versus your staleDaysThreshold) — urgencyScore is null when urgency is unknown.
  • issueKeywordBuckets and keywordsSupported — bounded matches against eight fixed keyword buckets (refund, rude_staff, cleanliness, wait_time, safety, billing, quality, cancellation) on text confirmed English or confirmed machine-translated to English; keywordsSupported: false when the language couldn't be confirmed.
  • reviewText/reviewTextTranslated — original/translated text, each truncated to 1000 characters with its own truncation flag.
  • publishedAtDate, responseFromOwnerDate, ageDays/ageBasis — strict calendar-valid ISO-8601 review-event dates (date-only accepted), age computed only when a valid observation reference exists — genuinely null/unknown otherwise, never guessed from wall-clock time.
  • responseDateTextConflict — true when a valid responseFromOwnerDate coexists with a blank/null or genuinely absent responseFromOwnerText, audited across every duplicate row sharing this reviewId, not just the newest one. A genuinely absent text only reaches this check when a supported keyword match elsewhere already cleared the row's usefulness gate; with no keyword match it is a free diagnostic before the conflict is ever evaluated.
  • evidence — the matched field, value, and rule behind every claim, tied to a sourceRowIndex.
  • Honest diagnostics for everything else — an invalid row, a source-reported failure, a missing identity, or insufficient evidence are all free, separate rows.

What this Actor is explicitly not: no AI-based review reading, no automated reply generation or sending, no calibrated sentiment-accuracy score, no broad or general-purpose language coverage. Reviewer names, profile IDs/URLs, and photos are never emitted, even if present in the source row.

Who uses it

  • Multi-location operators turning each new reviews-scraper batch into a triaged, prioritized response work queue instead of scrolling Google Maps by hand.
  • Customer-service leads who need a defensible, evidence-linked reason a review is flagged urgent, not an opaque score.
  • Reputation/marketing teams aggregating issue-keyword totals (refunds, staff conduct, cleanliness, wait times) across a location batch for weekly reporting.
  • Data-quality teams using responseDateTextConflict and keywordsSupported as a signal for how clean an upstream reviews scrape actually is.
  • Franchise and multi-brand groups running the same batch shape across dozens of locations, where a consistent, disclosed urgency rule matters more than any single location manager's judgment call.

None of these use cases require, and this Actor never claims to provide, an automated response draft, a sentiment score calibrated against real customer outcomes, or coverage of languages beyond confirmed-English/English-translated text.

Not a scraper: point it at an existing reviews Dataset producing the row shape in Input contract, most naturally Google Maps Reviews Scraper (users30d=6845, successfulRuns30d=12,829,388) or Google Maps Places Scraper (users30d=37173, successfulRuns30d=3,751,440). This Actor never starts either one, and never fetches a page from the open web.

How to run

  1. Console (fastest for a one-off run). Open the Actor, go to Input, paste rows into Source records (inline) or fill in Source Dataset, leave defaults, hit Start.
  2. From an existing reviews Dataset. Put its Dataset ID in datasetId — read with your own run's permissions, no separate authorization step.
  3. Chained via Integrations. Add this Actor as an "Actor/task run" integration on your reviews-scraper run; map {{resource.defaultDatasetId}} into this Actor's datasetId field explicitly (see Integration recipes) for a READ grant — a bare Dataset ID or webhook payload alone does not grant access.
  4. Via the API. Call POST /v2/acts/WgO1bFVK3eBJuKd7C/runs (or run-sync-get-dataset-items) with datasetId (or items) and, for a spending ceiling, the run option maxTotalChargeUsd.
  5. Set your SLA window. staleDaysThreshold (default 7) controls when age escalation kicks in; 4x that threshold adds a larger escalation.
  6. Read the results. One row per unique review lands in the run's default Dataset, with a matching OUTPUT/SUMMARY record.

Every run is independent processing — see Operating guide for what that means for re-running against the same batch.

Pricing

Pay-per-event, one event type: result-found ("Review assessment" in the platform's pricing UI — the primary pricing noun used throughout this listing, the Dataset terminology, and the pricing record itself) — one delivered review assessment (one unique reviewId). There is no start fee, and every free diagnostic (an invalid row, a source-reported failure, a missing identity, a budget stop, an empty batch) requests no charge event at all.

TierPrice per review assessmentDiscount
Free (default)$0.008—
Bronze$0.007210%
Silver$0.006420%
Gold$0.005630%
Platinum$0.005630%
Diamond$0.005630%

These are Apify's platform-wide account pricing tiers — not a volume schedule this Actor invents — assigned automatically by your account's usage; you don't select one. One review assessment is one unique reviewId, never billed per duplicate source row.

A note on the evidence in this README: every output example below comes from the product owner's own verification runs against this Actor's accepted build — cost to the owner, not sales or customer revenue. Source observation dates shown are the original, retained timestamps from that real snapshot, not a fresh re-scrape performed for this README.

Set the platform's own Max total charge (USD) run option to cap spend. If testing the zero-paid path via a raw API call, do not rely on literal maxTotalChargeUsd: 0 — use a small positive value below every tier's price (e.g. $0.005, below Gold/Platinum/Diamond's $0.0056) instead, and confirm the run's actual effective cap after it starts rather than trusting the requested value silently — see Evidence and boundaries.

Input contract

FieldTypeDefaultNotes
itemsarraycurated public prefillInline source rows; overridden by datasetId/runId/payload when set. Max 1000 items.
datasetIdstring (resourcePicker)—Existing source Dataset, read with your run's permissions. Overrides inline rows.
runIdstring—Alternative to datasetId: reads that run's own default Dataset with your run's permissions.
payloadobject—Optional ACTOR.RUN.SUCCEEDED webhook payload with resource.defaultDatasetId/resource.id. A bare payload does not grant access.
maxInputRowsinteger100 (max 1000)Source rows examined before grouping; also bounded to 4 MiB total, 64 KiB/row.
maxResultsinteger10 (max 1000)Useful review assessments delivered, independent of maxTotalChargeUsd.
observedAtstring (full ISO timestamp)empty / unknownOptional source observation reference used to compute review age; no wall-clock fallback. A date-only value or a timestamp without a timezone is rejected — this field always requires a full timestamp.
staleDaysThresholdinteger7Days before an unanswered review adds urgency points; 4x this adds a larger escalation.

Source precedence (never merged): explicit datasetId/payload.resource.defaultDatasetId > runId/payload.resource.id > inline items. A malformed or conflicting reference is rejected outright, not silently ignored.

Expected source row shape: reviewId, reviewUrl, text/textTranslated, stars, publishedAtDate, responseFromOwnerText, responseFromOwnerDate, placeId/title, originalLanguage/translatedLanguage. A row missing reviewId is a free missing_identity diagnostic. A responseFromOwnerText field that is genuinely absent from the row (never captured by the upstream scraper) is treated as unknown, distinct from a field the upstream scraper explicitly recorded as empty/null (an observed unanswered review) — the runtime does not conflate "never captured" with "confirmed empty."

Output examples

Example 1 — a clean unanswered review (final-prefill, real public snapshot, exact Dataset row).

{
"entityId": "Ci9DQUlRQUNvZENodHljRjlvT2trM2RXcG5WVmhoUW1kaVduWlVVSGs1YmpCTE1FRRAB",
"found": true,
"status": "unanswered",
"action": "respond_when_available",
"confidence": 1,
"sourceRowCount": 1,
"reviewId": "Ci9DQUlRQUNvZENodHljRjlvT2trM2RXcG5WVmhoUW1kaVduWlVVSGs1YmpCTE1FRRAB",
"reviewUrl": null,
"reviewUrlTruncated": false,
"placeId": "ChIJVVVVVVXlUVMRu-GPNDD5qKw",
"placeTitle": "Yellowstone National Park",
"placeTitleTruncated": false,
"stars": 5,
"publishedAtDate": "2026-09-22T22:12:29.184Z",
"ageDays": null,
"ageBasis": null,
"responseStatus": "unanswered",
"responseFromOwnerDate": null,
"urgency": "low",
"urgencyScore": 10,
"issueKeywordBuckets": [],
"keywordsSupported": true,
"originalLanguage": "en",
"translatedLanguage": null,
"reviewText": "Really great can't see everything in one visit in a relaxed pace but can see a lot over a few days, recommend that you pull off the road to view wildlife to avoid causing traffic or accidents, also don't bring pets a lot of the places in the park hav",
"reviewTextTruncated": false,
"reviewTextTranslated": null,
"reviewTextTranslatedTruncated": false,
"responseDateTextConflict": false,
"duplicateSourceRowCount": 1,
"evidence": [
{ "field": "responseFromOwnerText", "value": null, "rule": "observed_unanswered: source explicitly recorded no owner response", "sourceRowIndex": 0 },
{ "field": "publishedAtDate", "value": "2026-09-22T22:12:29.184Z", "rule": "age_unknown: publishedAtDate is valid but no observation reference (options.observedAt, row.scrapedAt, or context.observedAt) is available to compute age from", "sourceRowIndex": 0 },
{ "field": "publishedAtDate", "value": null, "rule": "urgency_partial: review age unknown (no valid options.observedAt, row.scrapedAt, or context.observedAt available), urgency score excludes the age component", "sourceRowIndex": 0 }
],
"businessPartial": true,
"sourceRowIndexes": [0],
"partial": true,
"sourceCoverage": { "complete": true, "reason": null, "droppedOversized": 0 },
"product": "google-reviews-response-backlog",
"schemaVersion": "1.0",
"sourceDatasetId": null,
"sourceRunId": null,
"suppliedSourceRunId": null,
"sourceRunIdVerified": null,
"processedAt": "2026-09-23T03:13:14.633Z"
}

ageDays/ageBasis are genuinely null here — no valid observedAt/scrapedAt was supplied in this run, so age is honestly unknown rather than derived from wall-clock time. sourceDatasetId/sourceRunId are null because this run used inline items, not a Dataset/run source.

Example 2 — a machine-translated review ("partial-columns" scenario), exact Dataset row. This actor's own adversarial-provenance.json states: "Historical label only. Actual source values retained; explicit found:true/partial:true injected as a controlled incomplete-source flag. This is a derived adversarial scenario, not observed upstream incompleteness or actual column removal." Every field below is unedited from the actor's own real partial-columns receipt — the "partial-columns" label describes the test scenario, not a claim that any column was actually removed from this row:

{
"entityId": "Ci9DQUlRQUNvZENodHljRjlvT21reGMwbHdTa2M1YVVWclpsUXpibkExVDE5SlltYxAB",
"found": true,
"status": "unanswered",
"action": "respond_when_available",
"confidence": 1,
"sourceRowCount": 1,
"reviewId": "Ci9DQUlRQUNvZENodHljRjlvT21reGMwbHdTa2M1YVVWclpsUXpibkExVDE5SlltYxAB",
"reviewUrl": null,
"reviewUrlTruncated": false,
"placeId": "ChIJVVVVVVXlUVMRu-GPNDD5qKw",
"placeTitle": "Yellowstone National Park",
"placeTitleTruncated": false,
"stars": 5,
"publishedAtDate": "2026-09-22T14:36:37.163Z",
"ageDays": null,
"ageBasis": null,
"responseStatus": "unanswered",
"responseFromOwnerDate": null,
"urgency": "low",
"urgencyScore": 10,
"issueKeywordBuckets": [],
"keywordsSupported": true,
"originalLanguage": "es",
"translatedLanguage": "en",
"reviewText": "Impresionante!!! Hermoso y unico!! Tiwnes que ir",
"reviewTextTruncated": false,
"reviewTextTranslated": "Amazing!!! Beautiful and unique!! You have to go",
"reviewTextTranslatedTruncated": false,
"responseDateTextConflict": false,
"duplicateSourceRowCount": 1,
"evidence": [
{ "field": "responseFromOwnerText", "value": null, "rule": "observed_unanswered: source explicitly recorded no owner response", "sourceRowIndex": 2 },
{ "field": "publishedAtDate", "value": "2026-09-22T14:36:37.163Z", "rule": "age_unknown: publishedAtDate is valid but no observation reference (options.observedAt, row.scrapedAt, or context.observedAt) is available to compute age from", "sourceRowIndex": 2 },
{ "field": "publishedAtDate", "value": null, "rule": "urgency_partial: review age unknown (no valid options.observedAt, row.scrapedAt, or context.observedAt available), urgency score excludes the age component", "sourceRowIndex": 2 }
],
"businessPartial": true,
"sourceRowIndexes": [2],
"partial": true,
"sourceCoverage": { "complete": true, "reason": null, "droppedOversized": 0 },
"product": "google-reviews-response-backlog",
"schemaVersion": "1.0",
"sourceDatasetId": null,
"sourceRunId": null,
"suppliedSourceRunId": null,
"sourceRunIdVerified": null,
"processedAt": "2026-09-23T03:13:27.014Z"
}

keywordsSupported: true here because translatedLanguage: "en" and textTranslated were both present — keyword matching ran against the English translation, never the Spanish original. found/partial are both already genuinely true from ordinary business logic (an unanswered review with an unknown age is a real partial result) — the adversarial scenario's own injection makes no visible difference on this particular row, which is exactly why the provenance note insists on calling it a historical label rather than an actual incompleteness signal.

Example 3 — below-price boundary run, exact free diagnostic row (real settled receipt). With maxTotalChargeUsd set to $0.005 (below every tier's $0.0056+ price), the run reads no source at all:

{
"product": "google-reviews-response-backlog",
"schemaVersion": "1.0",
"status": "budget_stopped",
"reason": "Run charge limit cannot cover any result; no source was read.",
"found": false,
"processedAt": "2026-09-23T03:13:40.264Z"
}

Example 4 — empty inline source, exact free diagnostic row (real settled receipt).

{
"product": "google-reviews-response-backlog",
"schemaVersion": "1.0",
"status": "no_results",
"found": false,
"reason": "no_usable_rows_in_batch",
"sourceCoverage": { "complete": true, "reason": null, "droppedOversized": 0 },
"processedAt": "2026-09-23T03:13:49.519Z"
}

Field dictionary

Result fields: entityId/reviewId (canonical, trimmed ID); status/responseStatus (answered/unanswered/unknown); urgency/urgencyScore (resolved/urgent(≥50)/moderate(≥20)/low, additive score or null when unknown); issueKeywordBuckets (subset of refund/rude_staff/cleanliness/wait_time/safety/billing/quality/cancellation); keywordsSupported (false when the language couldn't be confirmed English/English-translated); reviewText/reviewTextTranslated + reviewTextTruncated/reviewTextTranslatedTruncated (1000-char cap each); reviewUrl + reviewUrlTruncated (2048-char cap); placeTitle + placeTitleTruncated (500-char cap); placeId (type-validated, nulled if malformed); originalLanguage/translatedLanguage (35-char cap, nulled if malformed); publishedAtDate/responseFromOwnerDate (strict calendar-valid ISO-8601, date-only or full timestamp, nulled if malformed — never truncated into a fake date); ageDays/ageBasis (null unless a valid full-timestamp observation reference exists); responseDateTextConflict (see below); duplicateSourceRowCount/sourceRowCount/sourceRowIndexes; confidence; partial; evidence (up to 50 entries).

responseDateTextConflict: true when a valid responseFromOwnerDate is present alongside a blank/null or genuinely absent responseFromOwnerText, checked across every duplicate row sharing a reviewId — an older duplicate's contradiction is never masked by a newer, clean-looking one. An explicit blank/null text always reaches this check, since it already makes the row useful on its own. A genuinely absent text reaches it only when the row cleared the usefulness gate some other way — a supported keyword match on the review text; absent text with no keyword match fails the usefulness gate first and becomes a free insufficient_review_evidence diagnostic instead, never reaching the conflict check. When the conflict does apply, it forcibly reclassifies responseStatus/status/urgency/urgencyScore/statusBreakdown/urgencyBreakdown all to unknown, sets partial:true and action:"manual_verification_needed", and remains a paid, evidenced result.

Free Dataset diagnostic states: invalid_row (unusable row shape); source_reported_failure (a hard-failure signal — found:false, failed:true, or a status like failed/error/timed-out/aborted — always wins over a contradictory found:true/partial:true claim on the same row); missing_identity (no usable reviewId); invalid_identity_type; insufficient_source_data; insufficient_review_evidence (uniquely also carries responseStatus/stars/publishedAtDate/evidence alongside the diagnostic); invalid_domain_options; run-level budget_stopped/no_results/max_results_reached. A non-empty error/errors field with no hard-failure signal present is retained as a harmless warning ONLY when the same row explicitly carries both found:true and partial:true; in every other case it produces the free source_reported_failure diagnostic.

Run records (OUTPUT and SUMMARY) contain the same summary object. Their counters describe confirmed delivery, which can differ from raw Dataset rows after an ambiguous write or charge.

FieldMeaning
schemaVersion / productOutput contract version and this product's slug.
outcomedelivered, budget_stopped, max_results_reached, no_results, diagnostic, or failed.
errorCode / fatalFailureMachine-readable failure code or null, and whether the run ended in a fatal failure.
requestedSource rows counted within the read bound, including encountered oversized rows; not the requested useful-result limit.
usefulResultsUseful transform results after per-row oversized-result (size) filtering, counted before the later result-limit/budget delivery cap.
delivered / paidUseful rows whose Dataset write and result event are confirmed.
freeConfirmed free diagnostic writes; this can exceed the initial 200-diagnostic limit.
failedUseful results dropped because their serialized row exceeded 64 KiB.
deliveryUncertainA Dataset write or charge acknowledgment is uncertain, including FREE_WRITE_FAILED and DELIVERY_UNKNOWN; inspect raw rows and counters before retrying.
replayBlockedThis run's prior ledger claim prevented replay; this does not deduplicate a new run.
sourcePartial / sourcePartialReasonWhether source reading was incomplete and why: max_input_rows_reached, total_byte_cap_reached, oversized_rows_dropped, pagination_interrupted, invalid_json_page, unexpected_shape, or unexpected_page_size; otherwise null reason.
sourceDroppedOversizedSource rows skipped for exceeding the 64 KiB row bound.
budgetStopped / resultsCappedDelivery stopped at the charge limit or useful-result count limit.
businessPartialResultsAll partial transform results before delivery, size, budget or result-limit filtering; not a delivered count.
oversizedDiagnosticsDroppedDiagnostics that could not be written within the row-size bound.
diagnosticsTruncatedThe initial combined business/source-notice list exceeded 200 and was sliced; later notices are separate.
partialOverallOverall incompleteness from source, business results, dropped rows/diagnostics, diagnostic truncation, budget stopping or result-count capping.
budgetMode / maxTotalChargeUsdVerified charging mode and effective charge cap; these are run-level accounting state, not source metrics.
replaySafetyThe explicit warning that a new run is new processing and may charge again.
processedAtThis Actor's execution timestamp, not the upstream observation time or review publication date.
businessSummaryProduct summary: totalSourceRows, resultsCount, business-only diagnosticsCount, statusBreakdown, urgencyBreakdown, issueBuckets, and optional observation reference observedAt.

Additional free Dataset states are invalid_input, source_unavailable, partial_source, source_rows_dropped, and result_dropped_oversized. reason and error explain the condition when present; count reports dropped rows/results on the relevant notice; mode appears only on the source-resolution invalid_input/source_unavailable diagnostic and describes which source-resolution mode (dataset/inline/run/null) was attempted — never a budget mode, which is the separate run-level OUTPUT.budgetMode; delivered and totalUseful describe confirmed delivery versus available useful results on a stopping notice. sourceCoverage reports source complete, reason, and droppedOversized. Decorated rows also carry product, schemaVersion, source Dataset/run IDs, source row references, businessPartial, unified partial, and processedAt; early run-level diagnostics need not carry all decorated product fields. The overview's observedAt capture-time column may be blank when the source capture is unknown. Review event dates remain separate. The shared runtime deliberately supplies no processing-time fallback as context.observedAt.

Evidence and boundaries

No blanket 200-free-row cap, and businessSummary does not count every notice. The initial diagnostics array the shared runtime writes consists of this product's own report.diagnostics plus the runtime's own partial_source/source_rows_dropped notices, combined and then sliced to a 200-row write cap. businessSummary.diagnosticsCount counts business-only diagnostics — uncapped, so it can exceed 200. diagnosticsTruncated covers that combined (business + runtime-notice) list, not the business-only count. Separately, later notices — result_dropped_oversized, budget_stopped, max_results_reached — are written on top of the 200-row cap and are not counted against it. This actor's own mixed-boundary receipt (995 synthetic-failure rows ahead of 5 real rows, maxResults:1) confirms it exactly: businessSummary.diagnosticsCount: 995 (uncapped business count), 200 source_reported_failure rows actually written to the free cap, plus 1 additional max_results_reached notice on top — 201 total free rows, diagnosticsTruncated: true.

Hard-failure priority. isHardFailure always overrides a contradictory found:true/partial:true claim on the same row — a source cannot simultaneously report "this row failed" and "this row is usable." isSourceFailure with no hard-failure signal is a harmless warning only when the row explicitly carries both found:true and partial:true; otherwise it is the free source_reported_failure diagnostic.

Zero-charge cap normalization. A raw API maxTotalChargeUsd: 0 is normalized per a recorded observation — a positive value below every tier's price (Example 3 above used $0.005) reliably guarantees zero paid results, but the run's effective enforced cap should be inspected after the run starts, not simply trusted from the request.

Billing sequencing. Dataset-write happens before charge confirmation and is non-transactional. An ambiguous outcome finalizes the run as FAILED with no automatic retry and a possible unconfirmed row. There is no cross-run exactly-once delivery guarantee — a new run is new processing and can bill again.

Date semantics. publishedAtDate/responseFromOwnerDate are the review's own event/content timestamps — date-only is accepted, fixed at UTC midnight internally for age math, never reformatted in output. options.observedAt/row.scrapedAt/context.observedAt are the run's observation-time references and always require a full timestamp — a bare date is rejected there. When no valid reference exists at all, age is genuinely unknown (ageDays: null), never fabricated from code-execution wall-clock time.

Language gate. Keyword matching runs only against text confirmed originalLanguage en/en-* or confirmed translatedLanguage en/en-* with textTranslated present. An absent/unrecognized language is never assumed English.

Exact numeric and length caps, as coded, not paraphrased: MAX_EVIDENCE = 50 entries per result; MAX_TEXT_LENGTH = 1000 characters (reviewText/reviewTextTranslated); MAX_IDENTITY_LENGTH = 500 characters (reviewId, placeId where applicable); MAX_URL_LENGTH = 2048 characters (reviewUrl); MAX_TITLE_LENGTH = 500 characters (placeTitle); MAX_DATE_LENGTH = 64 characters (publishedAtDate, responseFromOwnerDate); MAX_LANGUAGE_TAG_LENGTH = 35 characters (originalLanguage, translatedLanguage). Shared runtime source-read bounds: at most 1000 rows, 4 MiB total source JSON, 64 KiB per row. A value exceeding its cap is truncated (text fields, with the matching ...Truncated flag set) or nulled (identity/date/language fields, since a truncated ID or date would be a fabricated value, not a shortened real one) rather than silently dropped or echoed verbatim.

Decision routing

  • responseStatus: "unanswered" + urgency: "urgent" → route to immediate response queue.
  • responseStatus: "unknown" (including a responseDateTextConflict reclassification) → route to manual verification, never to an automated "already answered" bucket.
  • keywordsSupported: false → exclude from keyword-driven issue reporting; still valid for status/urgency triage.
  • Free diagnostics (missing_identity, invalid_row, etc.) → route to a data-quality backlog for the upstream scraper, not the response queue.
  • issueKeywordBuckets containing refund or billing → route to finance/ops alongside the standard response queue, since these categories often need a decision beyond a customer-service reply.
  • duplicateSourceRowCount > 1 with conflicting per-occurrence evidence → treat the delivered assessment as the audited, cross-checked answer; don't re-derive status from a single duplicate row picked at random.

Commercial playbooks

  • Weekly response-backlog digest. Run against each location's latest reviews export; sort delivered rows by urgencyScore descending for a Monday-morning action list.
  • Issue-keyword rollup. Aggregate businessSummary.issueBuckets across a multi-location batch to spot a chain-wide refund or cleanliness spike before it shows up in survey data.
  • Data-quality audit for a reviews vendor. Use responseDateTextConflict and invalid_row/missing_identity diagnostic rates as an objective signal of how clean a vendor's export actually is.
  • SLA-conformance reporting. Compare urgencyBreakdown week over week after tuning staleDaysThreshold to your actual team response commitment, rather than relying on a manager's spot check of a handful of locations.
  • Franchise scorecards. Run the same staleDaysThreshold across every location in a group and rank locations by their unanswered/urgent share — a consistent, disclosed rule applied uniformly, not a subjective per-manager read of "how bad" a location's reviews look.

Integration recipes

Google Reviews Response Backlog Builder — workflow diagram

This Actor's ID is WgO1bFVK3eBJuKd7C. A natural upstream source is Google Maps Reviews Scraper (Actor ID Xb8osYTtOjlsgI6k9); Google Maps Places Scraper (compass~crawler-google-places) provides separate place-level context; its place rows are not interchangeable with review rows. The recipes below describe how to configure each mechanism — not a claim that any specific trigger is currently installed.

1. Console-native chaining (no code). On your reviews-scraper run page, open Integrations, add an "Actor/task run" action targeting this Actor, and let the Integrations UI populate the payload with {{resource.defaultDatasetId}}. Map that value explicitly into this Actor's datasetId field — not the generic payload pass-through alone — to request READ access under LIMITED_PERMISSIONS; the caller must already be authorized, and a private source can still deny access.

2. Direct webhook (POST /v2/webhooks), full recipe. Configure a webhook on your upstream reviews-scraper Actor for the ACTOR.RUN.SUCCEEDED event, scoped via condition.actorId, targeting this Actor's run-creation endpoint:

{
"isApifyIntegration": true,
"shouldInterpolateStrings": true,
"eventTypes": ["ACTOR.RUN.SUCCEEDED"],
"condition": { "actorId": "Xb8osYTtOjlsgI6k9" },
"requestUrl": "https://api.apify.com/v2/acts/WgO1bFVK3eBJuKd7C/runs?maxTotalChargeUsd=0.50",
"doNotRetry": true,
"headersTemplate": "{\"Authorization\": \"Bearer <APIFY_TOKEN>\"}",
"payloadTemplate": "{\"datasetId\": \"{{resource.defaultDatasetId}}\", \"payload\": {\"resource\": {\"defaultDatasetId\": \"{{resource.defaultDatasetId}}\", \"id\": \"{{resource.id}}\"}}, \"staleDaysThreshold\": 7}"
}

Submit this to POST https://api.apify.com/v2/webhooks with your own Authorization: Bearer <APIFY_TOKEN> header. The <APIFY_TOKEN> placeholder is never a literal token embedded in the template — it is filled in from your own secrets-provisioning store at the moment the webhook is registered, not an implicit platform template variable that resolves automatically. doNotRetry: true avoids a duplicate-charge risk from an automatic retry. datasetId is set explicitly — the resource picker or an authorized Integrations mapping can only request READ access; it never guarantees the platform actually grants it. A positive maxTotalChargeUsd on the request URL caps this chained run's spend; avoid the literal value 0 (see Evidence and boundaries).

3. Generic automation tools (n8n, Zapier, Make, or your own tooling). Use a generic Apify/HTTP node to call run-sync-get-dataset-items on this Actor's ID with a JSON body containing datasetId and your chosen staleDaysThreshold/maxResults — a plain authenticated HTTPS POST, no native Apify-integration support required. The same isApifyIntegration/payloadTemplate shape from recipe 2 applies to any integration definition your own tooling submits.

4. Export / helpdesk-adjacent mapping. Every run's Dataset supports CSV/Excel/JSON export via the Console or GET /v2/datasets/{datasetId}/items?format=csv. reviewId, placeTitle, status, urgency, urgencyScore, and issueKeywordBuckets alone drive a working response queue in a spreadsheet or helpdesk import.

Operating guide

  1. Confirm your source batch shape first. Spot-check rows against Input contract — each needs a usable reviewId, or you'll mostly get missing_identity diagnostics.
  2. Set staleDaysThreshold to your real SLA, not the default — it directly changes which reviews score urgent.
  3. Start with a small maxResults on a new source to confirm output matches expectations before a full batch.
  4. Set maxTotalChargeUsd via the platform run option, and check its effective value after the run starts. For a guaranteed-zero-paid API call, use a small positive value below $0.0056, not literal 0 — see Evidence and boundaries.
  5. Read the OUTPUT/SUMMARY record, not just the Dataset row count — it reports delivered/paid/free/failed, whether budgetStopped/resultsCapped ended the run early, and diagnosticsTruncated.
  6. If a run finalizes FAILED, inspect its Dataset and OUTPUT record before retrying — a blind retry can duplicate already-successful paid assessments.
  7. Re-running is always new billing — no cross-run dedup key. Reserve it for a new or updated batch, not a routine safety retry.

FAQ

Does this Actor scrape Google Maps itself? No. It only reads an existing reviews batch you or an upstream Actor already collected — no open-web fetch, no upstream Actor started.

Does it read reviews with AI or judge sentiment? No. Matching is literal, disclosed keyword-substring matching against eight fixed buckets on text confirmed English or confirmed English-translated — no negation handling, no sentiment score, no broad language coverage.

Does it send replies to reviewers? No. It produces a prioritized action recommendation (e.g. respond_when_available, manual_verification_needed) — it never contacts Google Maps or sends anything on your behalf.

What does a FAILED run mean? The runtime could not safely finish: source unavailability, algorithm/contract failure, invalid pricing or charge-limit state, a ledger/replay failure, an uncertain Dataset write or charge acknowledgment, free-diagnostic write failure, or failure to persist summaries can end the run. Dataset writing precedes charging; these are separate operations. A raw row can exist without a confirmed paid delivery. Inspect the Dataset, run event counters, and available OUTPUT/SUMMARY records before retrying; missing summaries are themselves a reason to reconcile. There is no automatic retry or promise that a failed run delivered nothing.

Is age/ageDays ever guessed from when the run happened to execute? No. Age is computed only from a valid full-timestamp options.observedAt/row.scrapedAt/context.observedAt; without one, ageDays is null — genuinely unknown, never backfilled from wall-clock time.

Can I pass a bare date like 2026-09-23 for observedAt? No — observedAt is a reference/meta timestamp and always requires a full ISO timestamp with a timezone (Z or an offset). A date-only value or a timestamp missing its timezone is rejected. Bare dates are only accepted for the review's own publishedAtDate/responseFromOwnerDate content fields.

What happens if the same review appears twice in my source batch? Every duplicate occurrence sharing a reviewId is counted in duplicateSourceRowCount and audited together — including for responseDateTextConflict, so an older duplicate carrying a contradiction is never masked by a newer, clean-looking copy. Only one assessment is delivered and billed per unique reviewId.

Does a non-empty error field on a source row always mean the row is dropped as a free diagnostic? Not always. Only an explicit hard-failure signal (found:false, failed:true, or a recognized failure-status alias) always forces source_reported_failure. Absent that, a stray error/errors value is kept as a harmless warning ONLY when the row explicitly carries both found:true and partial:true — any other combination still produces the free source_reported_failure diagnostic.

Is the "partial-columns" style example a demonstration of missing upstream data? No — see Output example 2. Same real values as Example 1's source snapshot; only partial/found are deliberately set as a controlled test, not a simulation of real column removal.

Why did one run report 201 free rows instead of the usual 200-row diagnostic cap? The initial business-diagnostic list is capped at 200 entries when written, but a later result-size/cap notice like max_results_reached is added on top and not counted against that cap — see Evidence and boundaries for this actor's own confirmed 995/200/201 numbers.

Sources and rights

Public source example: TInMu4Nq0if27nzls — a curated, non-sensitive snapshot of a real upstream reviews run, with original timestamps and provenance retained; reviewer names/profile IDs/photos were never captured in the example and are never emitted by this Actor's own output regardless of source content. All output examples and canary numbers in this README come from the product owner's own verification runs — cost to the owner, not customer sales or revenue.

Related tools for adjacent workflows in B2B lead generation and data enrichment.

ActorWhat it does
Structured Data ExtractorPair it in the B2B lead generation and data enrichment workflow: Extract title, meta, JSON-LD, headings, links, emails, and price observations from up to 100 authorized...
URL to Markdown ConverterPair it in the B2B lead generation and data enrichment workflow: Convert up to 100 authorized public HTML pages into clean Markdown for RAG, AI agents, SEO research, and...
Website Contact Scraper — Public Emails & PhonesPair it in the B2B lead generation and data enrichment workflow: Find public company-site emails, phones, and social profiles with page-level evidence, best-contact...

| Google Maps Scraper | Collect separate place-level context; map place identities to review rows before analysis. | | Google Maps Reviews Scraper | Collect the upstream reviews dataset; verify its fields against the input contract before chaining. |