Company Officer Appointment Overlap avatar

Company Officer Appointment Overlap

Pricing

from $21.00 / 1,000 appointment assessments

Go to Apify Store
Company Officer Appointment Overlap

Company Officer Appointment Overlap

Compare corporate appointments in Companies House datasets using exact officer IDs. Review reported intervals and cross-company overlaps with identity limits and source evidence preserved.

Pricing

from $21.00 / 1,000 appointment assessments

Rating

0.0

(0)

Developer

Tim Zinin

Tim Zinin

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

21 hours ago

Last modified

Share

Turn a batch of Companies House company records into a per-officer appointment assessment: exact officerId identity, cross-company overlap intervals with a stated comparison basis, and every duplicate-row contradiction kept visible instead of silently resolved.

The supplied scraper records expose opaque officerId values. This Actor links only exact matching IDs across company rows; it does not infer identity from names or personal attributes, verify registry identity independently, or prove that linked records represent the same individual.

Company Officer Appointment Overlap — buyer input flows through Companies House officer evidence to a reviewed overlap decision

What you get

Point this Actor at a dataset of Companies House company rows — each row carrying a nested officers array, the shape produced by scrapesage/companies-house-scraper — and it delivers one appointment assessment row per distinct officerId seen across the whole batch. Each row tells you:

  • How many distinct companies and appointment intervals this officer held in the batch. Exact-duplicate observations of the same interval (same company, role, start, end) reconcile into one interval, retaining every contributing source row on its evidence; a genuine re-appointment — a different date range at the same company after a gap — is never collapsed into the earlier one.
  • Whether any two intervals, at two different companies, overlap in time. A real closed-interval overlap test (max(startA, startB) <= min(endA, endB)) computed from the dates Companies House reported — never a name-based guess, never dependent on today's date. An impossible chronology (resignedOn earlier than appointedOn) is excluded from every overlap conclusion and disclosed separately via invalidChronologyAppointmentCount/chronologyInvalidPairsExcluded, never silently treated as "no overlap".
  • Contradictions: duplicate source rows reporting the same (company, role, start date) appointment with a different end date or status. status is an observation about an appointment, not part of its identity — two rows disagreeing only on status describe the SAME appointment carrying a live disagreement, and that forces confidence: 'low' and partial: true even when the overlap conclusion is otherwise clean.
  • Exact, uncapped counts wherever a list is display-capped: overlapPairsOmitted, contradictionsOmitted, evidenceOmitted, insufficientBasisPairCount, chronologyInvalidPairsExcluded, omittedSiblingOfficerEntries — never approximated as "displayed count plus one".
  • The source appointmentUrl, when supplied, retained as evidence on the corresponding appointment and overlap pair (appointmentUrlA/appointmentUrlB) — never fetched or resolved by this Actor, only passed through as a citation.

You do not get a claim that two appointments belong to the same real human being beyond what Companies House's own officerId already asserts, a "currently active" fact derived from a clock, a beneficial-ownership/control graph, or a legal/compliance/sanctions/credit opinion — see Evidence and boundaries.

Who uses it

  • Corporate/M&A due-diligence analysts who already run scrapesage/companies-house-scraper over a target's group structure and want a fast, auditable pass at which named officers sit on more than one company in that batch, with exact appointment dates and the official Companies House link ready to open.
  • KYC/AML and enhanced-due-diligence teams building a director-overlap picture as one input among several — this Actor's output is evidence for a human reviewer, not a pass/fail screening verdict, so it fits alongside (not instead of) sanctions, PEP, and adverse-media checks run separately.
  • Journalists and investigative researchers mapping interlocking directorships across a deliberately chosen set of companies, who need exact overlapping date ranges and an official source citation rather than a name-similarity guess.
  • Company secretariat / governance teams doing an internal conflict-of-interest sweep across a group's own subsidiaries, where asOf lets you ask "as of a specific stated date, which of our own directors also sit on another entity in this batch?" without relying on today's date.
  • Data teams building a downstream board-interlock dataset who want a pure, deterministic, non-LLM transform they can re-run identically against a refreshed pull and diff against prior output.

Not built for consumer background checks, individual identity verification, or any workflow needing proof of who a person is rather than which appointments Companies House's own records link together.

How to run

  1. Get source rows. Run scrapesage/companies-house-scraper (or an equivalent scrape producing the same row shape — companyNumber, companyName, scrapedAt, nested officers[] with officerId, name, appointmentUrl, status, role, appointedOn, resignedOn) for the companies you want to cross-check. Two rows are needed for a cross-company overlap; one row is still useful (single_company results with per-officer evidence).
  2. Point this Actor at that dataset. Paste the source datasetId into Source Dataset (a resourcePicker field — Apify resolves it with your own run's READ permission, never an elevated grant), or supply a runId. An Apify Integration template can populate payload.resource automatically — payload alone, without a resolving datasetId/runId, is never sufficient permission by itself.
  3. Leave items as the inline example, or override it. Source Dataset/runId, when set, overrides inline items entirely. The inline example ships as a real, curated snapshot (see Sources and rights), not a synthetic fixture, so you can press Start immediately.
  4. Set maxResults and maxInputRows to fit your batch and budget. maxResults (default 2) caps paid rows delivered; free diagnostics are never capped. maxInputRows (default 100, ceiling 1000) caps source rows read.
  5. Optionally set asOf (YYYY-MM-DD) for an explicit "as of this stated date" overlap reading instead of the default genuinely-open-ended treatment of appointments with no resignedOn. See Input contract.
  6. Set the platform's Max total charge run option for a hard USD ceiling — the preflight budget check (see Pricing) already refuses to start a paid pass if even one result cannot be afforded, giving a clean budget_stopped diagnostic instead of a partially-billed run.
  7. Read the dataset. Every delivered row carries product, schemaVersion, found, status, partial, sourceCoverage, sourceDatasetId/sourceRunId, processedAt plus its own fields — see Field dictionary and Output examples.

Pricing

One pricing noun, one billing event: appointment assessment (result-found). Diagnostics, budget-stop notices, source-partial notices, and empty-batch notices are always free — they never carry the result-found event, and they are never billed even when they are the only output of a run.

Apify tierPrice per appointment assessment
Free$0.0300
Bronze$0.0270 (10% off)
Silver$0.0240 (20% off)
Gold$0.0210 (30% off)
Platinum$0.0210 (30% off)
Diamond$0.0210 (30% off)

There is no separate start fee or per-run flat fee — you are charged only for delivered, useful rows, one result-found event per distinct officerId delivered. A run that produces zero useful results (empty batch, fully invalid batch, or a run stopped by your own budget before any result could be afforded) incurs no result-found event charge; owner validation and platform resource expenses are separate.

Billing is write-then-charge, non-transactional, and fails closed on ambiguity: the runtime writes a result row first, then requests the charge; if the charge cannot be confirmed, it does not silently retry (a retry risking a double charge is refused — see EXISTING_RESULT/EXISTING_CLAIM in Evidence and boundaries), and the run is marked failed rather than a clean success it cannot verify. An already-delivered result can, in a rare failure case, remain in the Dataset without a confirmed matching charge — chargedEventCounts.result-found (see settled.json in every scenario below) is the authoritative confirmed count and is not guaranteed to equal the visible useful-row count on every failure path. A new run against the same input is new processing and may bill again — no deduplication against a prior run's delivered officers.

Before reading any source, the runtime preflights affordability: if your charge ceiling (platform Max total charge) cannot cover even one result at your tier, the run stops immediately with a free budget_stopped diagnostic and reads no source rows — see below-price-r2 in Output examples.

Input contract

This Actor consumes the shared PPE (pay-per-event) source contract used across this batch of downstream Actors — a single JSON input object, read once at Actor start, never re-read mid-run:

FieldTypeRequiredNotes
itemsarrayNoInline source rows (max 1000 by schema; bounded to 1000 rows / 4 MiB total / 64 KiB per row by the runtime). Ships pre-filled with a real, curated Companies House snapshot. Ignored once datasetId/runId resolves a source.
datasetIdstring (resourcePicker, dataset, READ)NoExisting Dataset ID, resolved with your own run's permission. Overrides inline items. Integrations "Actor/task run" template maps {{resource.defaultDatasetId}} here automatically.
runIdstringNoActor run ID whose default Dataset is read instead, resolved with your own permission — this Actor never starts the upstream run itself.
payloadobjectNoSupplied resource envelope, not generated by the Actor. Malformed references or conflicting same-kind IDs are rejected; a private Dataset requires a READ grant. Accessible run metadata is checked; otherwise a readable explicit Dataset can proceed with unverified run provenance.
maxInputRowsinteger, 1–1000No (default 100)Caps source rows read. Rows beyond this are reported via sourcePartial/sourcePartialReason: 'max_input_rows_reached', never silently truncated.
maxResultsinteger, 1–1000No (default 2)Caps paid rows delivered. Free diagnostics are never capped. A free max_results_reached diagnostic states exact delivered/totalUseful — see below-price-r2/final-prefill-r2 in Output examples.
asOfstring, strict YYYY-MM-DDNoAn open-ended appointment (resignedOn: null) is treated as active only up to this date; an appointment not yet started as of it is excluded entirely — a stated assumption you chose, never today's date. An invalid value is rejected as a free invalid_input diagnostic before any source row is read.

Expected officers[] entry shape: officerId (required — an entry without one is not usable for identity and is counted, never silently dropped, via omittedSiblingOfficerEntries), name, appointmentUrl, status, role, appointedOn, resignedOn. Every entry of every row's officers array is scanned in full — no internal truncation beyond the shared 64 KiB per-row bound.

Two structural rules enforced before a row contributes anything: no usable companyNumber → free missing_company_number diagnostic; officers missing/not an array/empty → free missing_officers_array/empty_officers_array diagnostic. Neither ever produces a fabricated paid result.

Output examples

Every scenario below is the complete, unedited JSON array written to this Actor's Dataset by a real cloud run against the fixed round-2 code (build 0.1.1, buildId wa754rqx0ZTWuA96h), captured under ops/value-add-next-20260923/evidence/actors/company-officer-appointment-overlap/<scenario>/items.json, cross-checked against each run's own settled.json. Nothing below is paraphrased, trimmed, or re-ordered.

final-prefill-r2 — the shipped inline example, maxResults: 2

Real public TESCO PLC / TESCO STORES LIMITED source snapshot (see Sources and rights). settled.json for this run confirms chargedEventCounts.result-found: 2, "delivered: 2 paid, 1 free.". The batch's real total is 118 useful officers; maxResults: 2 caps delivery to the first two by sorted officerId, and the third row below discloses the exact totalUseful count instead of silently stopping.

[
{
"entityId": "officer:_iA0R7Mq82YzbEbtly0TfoRM7x8",
"found": true,
"status": "single_company",
"action": "review_appointments",
"confidence": "medium",
"officerId": "_iA0R7Mq82YzbEbtly0TfoRM7x8",
"professionalName": "WILLIAMS, Brian Richard",
"distinctCompanyCount": 1,
"appointmentCount": 1,
"invalidChronologyAppointmentCount": 0,
"overlapDetected": false,
"overlapPairs": [],
"overlapPairCount": 0,
"overlapPairsOmitted": 0,
"insufficientBasisPairCount": 0,
"chronologyInvalidPairsExcluded": 0,
"evidenceOmitted": 0,
"contradictions": [],
"contradictionCount": 0,
"contradictionsOmitted": 0,
"omittedSiblingOfficerEntries": 0,
"asOf": null,
"observedAt": "2026-09-23T06:41:57.760Z",
"sourceObservationWindow": {
"earliest": "2026-09-23T06:41:57.760Z",
"latest": "2026-09-23T06:41:57.760Z",
"validCount": 1,
"totalRowCount": 1
},
"evidence": [
{
"field": "appointment",
"companyNumber": "00519500",
"role": "Director",
"appointedOn": null,
"resignedOn": "1994-07-15",
"status": "Resigned",
"appointmentUrl": "https://find-and-update.company-information.service.gov.uk/officers/_iA0R7Mq82YzbEbtly0TfoRM7x8/appointments",
"chronologyInvalid": false,
"sourceRowIndex": 1
}
],
"businessPartial": false,
"sourceRowIndexes": [
1
],
"partial": false,
"sourceCoverage": {
"complete": true,
"reason": null,
"droppedOversized": 0
},
"product": "company-officer-appointment-overlap",
"schemaVersion": "1.0",
"sourceDatasetId": null,
"sourceRunId": null,
"suppliedSourceRunId": null,
"sourceRunIdVerified": null,
"processedAt": "2026-09-23T08:20:43.526Z"
},
{
"entityId": "officer:-8n4-Ctsi6TOgxR0XirDsMIVgLc",
"found": true,
"status": "multi_company_overlap",
"action": "review_overlap",
"confidence": "high",
"officerId": "-8n4-Ctsi6TOgxR0XirDsMIVgLc",
"professionalName": "NAWAZ, Imran",
"distinctCompanyCount": 2,
"appointmentCount": 2,
"invalidChronologyAppointmentCount": 0,
"overlapDetected": true,
"overlapPairs": [
{
"companyA": "00445790",
"roleA": "Director",
"appointedOnA": "2021-05-01",
"resignedOnA": null,
"appointmentUrlA": "https://find-and-update.company-information.service.gov.uk/officers/-8n4-Ctsi6TOgxR0XirDsMIVgLc/appointments",
"companyB": "00519500",
"roleB": "Director",
"appointedOnB": "2021-05-01",
"resignedOnB": null,
"appointmentUrlB": "https://find-and-update.company-information.service.gov.uk/officers/-8n4-Ctsi6TOgxR0XirDsMIVgLc/appointments",
"sourceRowIndexes": [
0,
1
]
}
],
"overlapPairCount": 1,
"overlapPairsOmitted": 0,
"insufficientBasisPairCount": 0,
"chronologyInvalidPairsExcluded": 0,
"evidenceOmitted": 0,
"contradictions": [],
"contradictionCount": 0,
"contradictionsOmitted": 0,
"omittedSiblingOfficerEntries": 0,
"asOf": null,
"observedAt": "2026-09-23T06:41:57.760Z",
"sourceObservationWindow": {
"earliest": "2026-09-23T06:41:53.170Z",
"latest": "2026-09-23T06:41:57.760Z",
"validCount": 2,
"totalRowCount": 2
},
"evidence": [
{
"field": "appointment",
"companyNumber": "00445790",
"role": "Director",
"appointedOn": "2021-05-01",
"resignedOn": null,
"status": "Active",
"appointmentUrl": "https://find-and-update.company-information.service.gov.uk/officers/-8n4-Ctsi6TOgxR0XirDsMIVgLc/appointments",
"chronologyInvalid": false,
"sourceRowIndex": 0
},
{
"field": "appointment",
"companyNumber": "00519500",
"role": "Director",
"appointedOn": "2021-05-01",
"resignedOn": null,
"status": "Active",
"appointmentUrl": "https://find-and-update.company-information.service.gov.uk/officers/-8n4-Ctsi6TOgxR0XirDsMIVgLc/appointments",
"chronologyInvalid": false,
"sourceRowIndex": 1
}
],
"businessPartial": false,
"sourceRowIndexes": [
0,
1
],
"partial": false,
"sourceCoverage": {
"complete": true,
"reason": null,
"droppedOversized": 0
},
"product": "company-officer-appointment-overlap",
"schemaVersion": "1.0",
"sourceDatasetId": null,
"sourceRunId": null,
"suppliedSourceRunId": null,
"sourceRunIdVerified": null,
"processedAt": "2026-09-23T08:20:43.526Z"
},
{
"status": "max_results_reached",
"reason": "maxResults (2) reached before all useful results were delivered.",
"delivered": 2,
"totalUseful": 118,
"partial": false,
"sourceCoverage": {
"complete": true,
"reason": null,
"droppedOversized": 0
},
"product": "company-officer-appointment-overlap",
"schemaVersion": "1.0",
"sourceDatasetId": null,
"sourceRunId": null,
"suppliedSourceRunId": null,
"sourceRunIdVerified": null,
"processedAt": "2026-09-23T08:20:43.686Z",
"found": false
}
]

The second row is the real proof of the officerId-only identity rule: NAWAZ, Imran was appointed Director at both TESCO PLC (00445790) and TESCO STORES LIMITED (00519500) on the exact same date under the same officerId, giving overlapDetected: true with one clean, high-confidence pair and both appointmentUrl citations retained. The curated sample also contains the opposite case (not shown in this capped run): "TAYLOR, Christopher Jon" appears at both companies under two different officerId values — Companies House treats them as separate identities, so this Actor reports two separate single_company results rather than a false-positive overlap from the matching name.

partial-columns-r2 — bounded source (maxInputRows: 1)

This scenario name is a legacy label from the shared naming convention: it means the run was bounded by maxInputRows: 1, not that any column/field was deleted — every field of the one row actually read is unchanged. settled.json confirms "delivered: 2 paid, 2 free.", sourceCoverage.reason: "max_input_rows_reached" on every row.

[
{
"status": "partial_source",
"reason": "max_input_rows_reached",
"processedAt": "2026-09-23T08:22:11.097Z",
"partial": true,
"sourceCoverage": {
"complete": false,
"reason": "max_input_rows_reached",
"droppedOversized": 0
},
"product": "company-officer-appointment-overlap",
"schemaVersion": "1.0",
"sourceDatasetId": null,
"sourceRunId": null,
"suppliedSourceRunId": null,
"sourceRunIdVerified": null,
"found": false
},
{
"entityId": "officer:-8n4-Ctsi6TOgxR0XirDsMIVgLc",
"found": true,
"status": "single_company",
"action": "review_appointments",
"confidence": "medium",
"officerId": "-8n4-Ctsi6TOgxR0XirDsMIVgLc",
"professionalName": "NAWAZ, Imran",
"distinctCompanyCount": 1,
"appointmentCount": 1,
"invalidChronologyAppointmentCount": 0,
"overlapDetected": false,
"overlapPairs": [],
"overlapPairCount": 0,
"overlapPairsOmitted": 0,
"insufficientBasisPairCount": 0,
"chronologyInvalidPairsExcluded": 0,
"evidenceOmitted": 0,
"contradictions": [],
"contradictionCount": 0,
"contradictionsOmitted": 0,
"omittedSiblingOfficerEntries": 0,
"asOf": null,
"observedAt": "2026-09-23T06:41:53.170Z",
"sourceObservationWindow": {
"earliest": "2026-09-23T06:41:53.170Z",
"latest": "2026-09-23T06:41:53.170Z",
"validCount": 1,
"totalRowCount": 1
},
"evidence": [
{
"field": "appointment",
"companyNumber": "00445790",
"role": "Director",
"appointedOn": "2021-05-01",
"resignedOn": null,
"status": "Active",
"appointmentUrl": "https://find-and-update.company-information.service.gov.uk/officers/-8n4-Ctsi6TOgxR0XirDsMIVgLc/appointments",
"chronologyInvalid": false,
"sourceRowIndex": 0
}
],
"businessPartial": false,
"sourceRowIndexes": [
0
],
"partial": true,
"sourceCoverage": {
"complete": false,
"reason": "max_input_rows_reached",
"droppedOversized": 0
},
"product": "company-officer-appointment-overlap",
"schemaVersion": "1.0",
"sourceDatasetId": null,
"sourceRunId": null,
"suppliedSourceRunId": null,
"sourceRunIdVerified": null,
"processedAt": "2026-09-23T08:22:11.157Z"
},
{
"entityId": "officer:4572Jha9rRwa8EciipP-UgTBqLQ",
"found": true,
"status": "single_company",
"action": "review_appointments",
"confidence": "medium",
"officerId": "4572Jha9rRwa8EciipP-UgTBqLQ",
"professionalName": "MACLAURIN, Ian Charter, Lord",
"distinctCompanyCount": 1,
"appointmentCount": 1,
"invalidChronologyAppointmentCount": 0,
"overlapDetected": false,
"overlapPairs": [],
"overlapPairCount": 0,
"overlapPairsOmitted": 0,
"insufficientBasisPairCount": 0,
"chronologyInvalidPairsExcluded": 0,
"evidenceOmitted": 0,
"contradictions": [],
"contradictionCount": 0,
"contradictionsOmitted": 0,
"omittedSiblingOfficerEntries": 0,
"asOf": null,
"observedAt": "2026-09-23T06:41:53.170Z",
"sourceObservationWindow": {
"earliest": "2026-09-23T06:41:53.170Z",
"latest": "2026-09-23T06:41:53.170Z",
"validCount": 1,
"totalRowCount": 1
},
"evidence": [
{
"field": "appointment",
"companyNumber": "00445790",
"role": "Director",
"appointedOn": null,
"resignedOn": "1997-06-06",
"status": "Resigned",
"appointmentUrl": "https://find-and-update.company-information.service.gov.uk/officers/4572Jha9rRwa8EciipP-UgTBqLQ/appointments",
"chronologyInvalid": false,
"sourceRowIndex": 0
}
],
"businessPartial": false,
"sourceRowIndexes": [
0
],
"partial": true,
"sourceCoverage": {
"complete": false,
"reason": "max_input_rows_reached",
"droppedOversized": 0
},
"product": "company-officer-appointment-overlap",
"schemaVersion": "1.0",
"sourceDatasetId": null,
"sourceRunId": null,
"suppliedSourceRunId": null,
"sourceRunIdVerified": null,
"processedAt": "2026-09-23T08:22:11.157Z"
},
{
"status": "max_results_reached",
"reason": "maxResults (2) reached before all useful results were delivered.",
"delivered": 2,
"totalUseful": 70,
"partial": true,
"sourceCoverage": {
"complete": false,
"reason": "max_input_rows_reached",
"droppedOversized": 0
},
"product": "company-officer-appointment-overlap",
"schemaVersion": "1.0",
"sourceDatasetId": null,
"sourceRunId": null,
"suppliedSourceRunId": null,
"sourceRunIdVerified": null,
"processedAt": "2026-09-23T08:22:11.278Z",
"found": false
}
]

Every result row here carries partial: true — not because any officer's own evidence was incomplete, but because sourceCoverage.complete: false propagates honestly from the run-level bound onto every row it could have affected, so a buyer reading only one row's partial flag still learns the batch was bounded.

below-price-r2 — preflight budget refusal, source never read

maxTotalChargeUsd: 0.005 set below the cost of one result. settled.json confirms chargedEventCounts.result-found: 0, "budget_stopped: 0 paid, 1 free." — the preflight guard refuses the source read once it determines the charge ceiling cannot cover one result.

[
{
"product": "company-officer-appointment-overlap",
"schemaVersion": "1.0",
"status": "budget_stopped",
"reason": "Run charge limit cannot cover any result; no source was read.",
"found": false,
"processedAt": "2026-09-23T08:21:48.930Z"
}
]

empty-source-r2 — a batch with no usable rows

[
{
"product": "company-officer-appointment-overlap",
"schemaVersion": "1.0",
"status": "no_results",
"found": false,
"reason": "no_usable_rows_in_batch",
"sourceCoverage": {
"complete": true,
"reason": null,
"droppedOversized": 0
},
"processedAt": "2026-09-23T08:21:39.391Z"
}
]

On a successful empty-input path the runtime attempts to write this free diagnostic. A storage failure may prevent delivery or summary persistence; inspect run status and counters before interpreting an empty Dataset.

Field dictionary

Every field delivered on a found:true useful-result row, beyond the shared runtime envelope (product, schemaVersion, found, status, action, confidence, partial, businessPartial, evidence, sourceRowIndexes, sourceCoverage, sourceDatasetId, sourceRunId, suppliedSourceRunId, sourceRunIdVerified, processedAt):

FieldMeaning
officerIdThe exact, opaque Companies House officerId — the sole identity key linking appointments across companies. Never derived from name, DOB, nationality, address, or occupation.
professionalNameFirst non-empty professional name for this officerId — display only, never used for matching. null if none supplied.
distinctCompanyCountDistinct companyNumber values with at least one appointment entry for this officer.
appointmentCountDistinct appointment intervals for this officerId, after reconciling exact-duplicate observations of the same (company, role, appointedOn, resignedOn) tuple into one interval — source-row attribution retained subject to the documented display bounds. A genuinely different date range at the same company (e.g. re-appointment after resignation) is always kept separate.
invalidChronologyAppointmentCountDistinct intervals where resignedOn is strictly before appointedOn — an impossible interval, still shown in evidence (chronologyInvalid: true) but excluded from every overlap conclusion.
overlapDetectedtrue when at least one pair of intervals at two different, chronology-valid companies intersects in time with both appointedOn known. A company-internal re-appointment is never counted. false only means no qualifying overlap was found in the supplied batch.
overlapPairs / overlapPairCount / overlapPairsOmittedOverlapping company/role/date pairs (capped at 10, each with sourceRowIndexes and appointmentUrlA/appointmentUrlB), the exact total found, and the exact count not displayed.
insufficientBasisPairCountDistinct-company pairs where overlap could not be evaluated because appointedOn was missing/invalid on at least one side — an explicit gap, never "no overlap".
chronologyInvalidPairsExcludedDistinct-company pairs excluded because at least one side had an impossible interval — separate from insufficientBasisPairCount.
evidence / evidenceOmittedBounded list (cap 12) of intervals — companyNumber, role, appointedOn, resignedOn, status, appointmentUrl, chronologyInvalid, sourceRowIndex — and the exact count not shown.
contradictions / contradictionCount / contradictionsOmittedBounded list (cap 10) of duplicate-row disagreements on end date or status of the same (company, role, start) appointment, exact total, exact omitted count. As of the round-2 fix, a same-interval status disagreement is included here (see Evidence and boundaries).
omittedSiblingOfficerEntriesSum, across every row this officer appears in, of nested officers entries that were not usable (not an object, or no officerId) — counted once per unique contributing officer per row, never inflated by repeats within one row.
asOfThe asOf date supplied, echoed back; null when omitted.
observedAt / sourceObservationWindowLatest/earliest valid scrapedAt capture timestamp across rows contributing to this officer, compared by normalized instant — a capture time, never an appointment event date, never "now". sourceObservationWindow also reports validCount/totalRowCount.
sourceRowIndexesDistinct original-dataset row indexes this officer's appointments were drawn from, remapped to the true source offset, capped at 50.

Evidence and boundaries

What this Actor deliberately does not claim, matching the source contract in SPEC.md exactly:

  • Not identity, not disambiguation. officerId matching is proof of Companies House's own appointment-record linkage, not proof that two appointments belong to the same real individual, and not a person-disambiguation service. If Companies House ever issued the same officerId to two different people (a source-side error this Actor cannot detect), it would report them as one officer — it trusts the source identity key as given.
  • A missing resignedOn is not proof of a currently active appointment. status: 'Active' or resignedOn: null is the source's own claim, observed at its scrape time (observedAt) — never re-asserted here as a fact about today. Without asOf, an open-ended appointment is compared as genuinely open; no wall-clock is ever read inside the transform.
  • A missing appointedOn on either side of a potential pair is never guessed at. It is counted in insufficientBasisPairCount, distinct from both "no overlap" and "overlap".
  • No beneficial ownership, ultimate control, or officer-ID fabrication. Never infers a beneficial-ownership graph or control percentage, and never invents an officerId when a nested entry lacks one — such an entry is counted via omittedSiblingOfficerEntries, never assigned a synthetic ID.
  • No DOB, home address, nationality, or occupation in any output, even if a source row happened to carry one.
  • No legal, compliance, sanctions, credit, or investment merit judgment. confidence (high/medium/low) describes evidence coverage and internal consistency, never a legal or commercial verdict. action is a suggested next step for a human reviewer, not an automated decision.
  • Conflicting status observations. The same company/role/start/end tuple remains one appointment when status differs. Status variants become explicit contradictions, which force low confidence and partial coverage.
  • The round-2 sibling-omission fix, precisely. omittedSiblingOfficerEntries was incremented once per nested occurrence of a contributing officer within one row, double-counting a repeated officer. Fixed: unique contributing officerIds are collected into a set first, incrementing each bucket exactly once per unique officer per row.
  • Replay and billing honesty. EXISTING_RESULT/EXISTING_CLAIM guard against re-processing an already-claimed run; a chargedEventCounts.result-found mismatch halts further billing rather than risk a duplicate charge. Delivery is write-then-charge and non-transactional, so a Dataset write can succeed while charge confirmation cannot — the run is then reported failed, with the honest delivered count in the message, never a false "succeeded". Summary/OUTPUT persistence is itself best-effort: a failure there also fails the run (FINALIZATION_FAILED); dataset rows/logs remain the source of truth.

Decision routing

A suggested triage order for a batch of delivered rows, not an automated verdict:

  1. status: multi_company_overlap, confidence: high, contradictionCount: 0 — the cleanest signal this Actor produces: both intervals chronology-valid, both start dates known, no duplicate-row disagreement. Open the appointmentUrlA/appointmentUrlB citations and verify against the official record before acting.
  2. status: multi_company_overlap, confidence: low (contradictionCount > 0) — the overlap may still be real, but at least one contributing appointment carries an unresolved disagreement. Resolve it (or re-scrape) before treating the overlap as settled.
  3. status: single_company or multi_company_no_overlap — check insufficientBasisPairCount/chronologyInvalidPairsExcluded before concluding "no overlap exists" — a nonzero value means real cross-company pairs this Actor explicitly could not evaluate, not that it evaluated them and found none.
  4. Any row with partial: true — read sourceCoverage and the omission counts (evidenceOmitted, overlapPairsOmitted, contradictionsOmitted, omittedSiblingOfficerEntries); the count fields are always exact even when a displayed list is capped.
  5. Free diagnostic rows (max_results_reached, budget_stopped, partial_source, no_results, source_rows_dropped) — run-level signals, not officer-level results. max_results_reached's totalUseful tells you exactly how many more paid rows exist if you raise maxResults and re-run (billing again only for the additional rows, per the no-dedup replay policy).

Commercial playbooks

  • Group-structure conflict sweep before a board appointment or advisory engagement. Scrape the acquirer's own subsidiary group plus the target's known group, run this Actor across the combined batch with a stated asOf, and treat any multi_company_overlap row touching both groups as a disclosure item for legal/compliance — a documented lead with a citation, not an automatic conflict finding.
  • Enhanced due diligence alongside sanctions/PEP screening. This Actor's overlap evidence is explicitly not a sanctions or PEP determination (see Evidence and boundaries); pair it with a dedicated screening Actor so a director flagged elsewhere gets escalated with two independent, separately-sourced pieces of evidence.
  • Interlocking-directorate research for competitive intelligence or journalism. Batch a peer set of companies, run with a generous maxResults, and treat every multi_company_overlap row as a lead worth the official citation — never a finished claim.
  • Recurring internal governance audit. Schedule a periodic re-run (platform scheduler or n8n/Make cron — see Integration recipes), diffing delivered officerId sets between runs to surface newly-appeared cross-entity appointments; each scheduled run is new processing and bills again per the no-dedup policy.
  • Pre-transaction officer cross-reference for a small, named counterparty list. A small maxInputRows/maxResults run is cheap and gives an auditable, citation-backed first pass before a full manual Companies House review.

Integration recipes

Apify Integration setup. Map {{resource.defaultDatasetId}} explicitly into Source Dataset (datasetId) and configure its READ resource grant. You can additionally supply the matching resource envelope as payload. The envelope selects a source but does not grant access to a private Dataset; the empty default does not populate an envelope automatically.

A bounded, explicit Apify API call (using your own API token, never embedded in this README):

POST https://api.apify.com/v2/acts/zinin~company-officer-appointment-overlap/runs?maxTotalChargeUsd=0.75
Authorization: Bearer <YOUR_API_TOKEN>
Content-Type: application/json
{
"datasetId": "<upstream Companies House Dataset ID you already control READ access to>",
"maxInputRows": 200,
"maxResults": 20,
"asOf": "2026-09-23"
}

datasetId is a resourcePicker-backed field — the platform enforces that your API token's own READ permission on that Dataset is what is checked, never an elevated grant carried by this Actor. A payload envelope with no resolving datasetId/runId inside it is never sufficient permission on its own — see Input contract.

A clearly-labelled webhook registration TEMPLATE — an illustrative registration against Apify's own webhook API to be notified when a run finishes, not a captured execution, and it deliberately omits any real token:

{
"eventTypes": [
"ACTOR.RUN.SUCCEEDED",
"ACTOR.RUN.FAILED"
],
"requestUrl": "https://your-own-endpoint.example.com/webhooks/officer-overlap",
"headersTemplate": "{\"Authorization\": \"Bearer <YOUR_OWN_ENDPOINT_SECRET>\"}",
"payloadTemplate": "{\"runId\":\"{{resource.id}}\",\"datasetId\":\"{{resource.defaultDatasetId}}\",\"status\":\"{{resource.status}}\"}",
"doNotRetry": true
}

doNotRetry: true is deliberate: because delivery is write-then-charge and non-transactional (see Pricing), an automatic webhook retry on your receiving endpoint could re-process an already-delivered run's notification as a new one. Treat a missed webhook as a signal to poll run status via the API, not a reason to auto-retry.

Company Officer Appointment Overlap — scheduled trigger through this Actor into a CRM or governance review queue

n8n / Make: use the native Apify node (or a generic HTTP node against the API call above) to trigger a run on a schedule or the upstream scraper's completion webhook, then branch on each delivered row's status/confidence to route multi_company_overlap rows into a review queue or CRM task, and free diagnostic rows into an operational alert channel.

MCP / agent tooling: plain JSON input/output with no product-specific authentication beyond the shared Dataset resourcePicker model, so it composes directly into an agent tool-call chain that already holds a Companies House Dataset reference.

Operating guide

  • Start small, then widen. Run with a low maxInputRows/maxResults against a known batch first (the shipped example is real and free to inspect at maxResults: 2), confirm the field shapes match your downstream system, then raise the caps for production.
  • asOf is a stated assumption, not a live status check. Use it for a reproducible "as of this date" reading for an audit trail — never to approximate "as of today," since no clock is ever read. Omitting asOf gives the more conservative, open-interval reading.
  • A capped display list does not mean capped truth. overlapPairCount, contradictionCount, evidenceOmitted are always exact, independent of the 10/10/12-item display caps. A nonzero omitted count on a row you care about is signal enough to escalate, or partition that officer’s source rows into smaller batches, inspecting omitted counts on each. Isolating one officer alone does not remove per-officer display caps; deduplicate overlapping partitions.
  • partial: true is diagnostic, not a defect. It can mean the batch was bounded, a display list was capped, a contradiction was found, or a chronology was excluded — check the specific counts (per Field dictionary).
  • Budget your run before you run it. Set the platform's Max total charge to comfortably cover your intended maxResults at your tier; the preflight guard (see Pricing) fails an under-budgeted run cleanly and for free before reading any source.
  • Re-runs are not deduplicated. For "only new overlaps since last time," diff delivered officerId sets between runs downstream — replay safety only refuses double-billing of the same run's own already-confirmed results.
  • Treat source text as observations. Role and dates participate in appointment grouping; conflicting status values produce contradictions. Display bounds do not turn an observed value into verified employment or identity.

FAQ

Does an overlap result prove the same real person holds both appointments? It proves Companies House's own record already links both appointments to the same officerId — this Actor never performs its own name/DOB/address matching. See Evidence and boundaries.

If overlapDetected is false, does that mean the officer has no other appointments? No — only that no overlap was found in your supplied batch with a usable date basis on both sides. Check insufficientBasisPairCount first.

Does resignedOn: null mean the officer is actively serving today? No — it means the source reported no resignation date as of its own scrape time (observedAt), never re-asserted here as a live fact.

Can this Actor tell me who ultimately owns or controls a company? No. Appointment-interval overlaps only; beneficial ownership, PSC, and shareholding are out of scope and never fabricated.

Why did my run deliver fewer paid rows than expected? Check the free diagnostics: max_results_reached states exact delivered/totalUseful (raise maxResults and re-run — bills again for the additional rows only); budget_stopped means your charge ceiling was reached; partial_source means the source batch was bounded.

What happens if the same officerId appears with two different statuses across duplicate rows? As of the round-2 fix, one appointment interval carrying an explicit contradiction, confidence: 'low', partial: true — never two separate appointments.

Is this Actor's result a KYC/AML pass or fail? Neither — confidence/action are evidence-coverage and next-step signals. Pair with dedicated sanctions/PEP/adverse-media tools for a screening decision.

Will running this Actor again on the same input cost nothing the second time? No — a new run bills again for every delivered paid row, per summary.replaySafety. Diff results downstream for "only what's new."

Is this Actor publicly listed and independently accepted yet? Not as of this README — round-2 defects are fixed and re-tested (33/33), but ROOT/Jev final acceptance and public listing are still pending.

Sources and rights

Source data originates from the UK's Companies House public register, via the scrapesage/companies-house-scraper Actor's documented output shape. This Actor never calls the Companies House API or website directly — it only transforms rows you already collected through your own upstream source.

The inline example shipped with this Actor's input schema is a real, curated projection of an actual scrapesage/companies-house-scraper run (actorId BWcp1XUouKEuCvY41, runId 8e2TzivdAS0GOolv0, datasetId qna0XkdGqOjjELEov, captured 2026-09-23) — public business-record fields only, values unchanged, with an added __source object disclosing provenance. It is a snapshot, not a live query on every run; re-run the upstream scraper for current data. The saved public projection omits DOB, home address, nationality, and occupation fields. Inspect your own input for private data before sharing it.

Source acquisition and reuse remain subject to the registry and upstream provider’s terms. Source appointment URLs, when present, let a reviewer consult the original record; this Actor does not fetch or verify them.

Related Zinin/ Actors you may already have in your account* (each independently public and priced — adjacent workflow context, not a claim their rows are directly compatible input to this Actor):

  • zinin/company-registry-enricher — registry lookups to assemble the initial company list before feeding this Actor.
  • zinin/new-company-detector — flags newly-incorporated companies, a useful trigger for a recurring overlap sweep on a tracked sector.
  • zinin/litigation-check — separate, independently-sourced litigation evidence for the same named companies; run as its own pass, no shared data model.
  • zinin/sec-edgar-watcher — for a US-linked counterparty, a complementary SEC-filing watch; no shared input format, point it at its own source.