This page is written for an AI agent building a Kardinal integration from a client’s raw data (a spreadsheet, a CRM export, a verbal description of their operation) — not for a human developer exploring the API. If you are an agent doing this kind of work, read this in full before writing a single line of code or calling any endpoint. If you are human, this is still useful context, but First API call is the better starting point.
Every point below comes from observed failure modes, not hypothetical ones — each one caused a real, measurable gap between an agent’s payload and a correct reference plan for the same client data.
1. Don’t silently default on an irreducible ambiguity — ask
Some questions genuinely cannot be answered from the API documentation or from the client’s raw data, because the answer depends on operational tolerances only the client knows (how bad is a late delivery, really? is a truck legally required for this route?). When you hit one:
- Do not pick a default and move on silently.
- Do produce an explicit, visible list of open questions requiring client confirmation, separate from the rest of your modeling notes — one item per ambiguity, stating what you assumed in the meantime and why.
- This includes binary policy decisions — a boolean or a choice between two options that isn’t derivable from the data (for example, whether resources are allowed to compact idle time instead of always departing as early as possible). A two-way choice is still an open question: it belongs on this same visible list, not as a footnote tucked away among other modeling notes elsewhere in your report, where it’s easy for a reviewer to miss.
- List every field you chose not to set at all, not only the ones where you inferred a risky value. A field with zero trace in the source data still belongs on the list (“not set — no source data for this”) — a field that’s simply absent from your report, with no mention anywhere, is indistinguishable to a reviewer from an oversight you didn’t notice.
- Don’t rely on remembering which optional fields matter from context — that’s exactly the habit that lets one get silently dropped. Instead, before finalizing your report, run through the full set of optional/placeholder-prone fields on every object type you populated (resource-level fields like
agencyId, maxWorkingDuration, breaks, maxDistanceInKm; stop-level fields like departure/arrival, lateDeparture, secondary capacities dimensions beyond the primary one) and state the status of each explicitly — set from data, defaulted, or not set — rather than only mentioning the subset you happened to think of unprompted. A field you correctly handled last time is not evidence you’ll remember to check a different field this time; treat this as a fixed pass over the whole set, every time, not a memory exercise.
- A prose reminder to “state the status of each field” has repeatedly failed to survive contact with an actual delivery, even when the exact field name involved was already spelled out above, in this exact document, ahead of time — reading the rule is not the same as applying it under the pressure of finishing a deliverable. Don’t rely on prose alone a second time: deliver the field-status pass above as a literal table (one row per field in the list, columns for field name / status / value-or-reason), inline in your modeling report, not as a paragraph summarizing the outcome. A table has no room for a field to be silently absent the way a sentence does — an omitted row is visibly a blank spot in a structure the reviewer can check row-by-row, where an omitted clause in a paragraph is not. If your delivery format can’t render a table, use an equivalent fixed key: value list with one line per field, in the same fixed order every time — the point is a structure a reviewer (or a script) can diff against the full field list, not free-form prose that happens to mention most of it.
- Resolving a structural decision with confidence in your own arithmetic is not the same as confidence in the assumption that arithmetic depends on. If a structural choice (for example, how many loops a route needs, or which pattern applies) follows deterministically from a value you yourself estimated because the real one was missing from the data (a fleet size, a demand figure, a headcount), state the choice as contingent on that estimate — “we chose X because we sized Y ourselves; if the real Y turns out to differ, this choice would flip” — rather than presenting the downstream decision as settled just because the math that produced it was done correctly.
- When two fields are documented as a linked pair — one only makes sense in light of the other, for example a wide working-time window combined with a duration cap, where the two together determine what the payload actually means — don’t address one half while silently leaving the other unaddressed. If you set or explicitly flag one half of such a pair, state the status of the other half too, even if that status is “not set” or “left at default,” and note the relationship between the two if it isn’t obvious from the field names alone. Leaving one half silently unaddressed doesn’t fall back to some neutral default meaning — it produces a third, different, unflagged meaning that no one asked for and that no reviewer was ever told to expect.
A payload that is 100% internally consistent but built on unstated, unconfirmed business assumptions is not a safe deliverable — it’s a set of guesses with good formatting. The sections below are specific, recurring cases of this same principle.
2. Check capacity feasibility before modeling orders — and don’t resolve a shortfall on your own
Before you settle on the aggregation level used to group source rows into an Order (for example, a delivery-note ID, an order/consignment number, or some other row-level key), check the relationship between that level and its neighbors in both directions: verify that no group at your chosen level mixes rows that belong to different underlying entities (a many-to-one check against the next finer level), and also check whether a single customer or site has more than one occurrence of your chosen level on the same day — something a real operation might consolidate into a single visit (the many-to-one check against the next coarser level, such as the customer or site itself). Passing only the first check doesn’t prove your chosen level is the one real operations actually uses: there can be a coarser level that production consolidates onto, with nothing in the source data explicitly flagging the gap.
Even after this two-direction check passes, cross-check the resulting grouping against any independent referential already present in the source data — for example, a canonical driver-vehicle pairing table — before treating a plausible-looking grouping as final: a group that looks structurally consistent can still combine rows that the client’s own referential data says shouldn’t go together. Also scan free-text note or comment fields for explicit consolidation hints (a note stating a delivery is “same as” or “same zone as” another) — this is exactly the kind of signal a purely structural or positional check will miss, since it lives in prose rather than in the fields you’re joining on.
Before finalizing how you model orders, add up the total demand (weight, volume, or whatever capacity unit the client uses) across all stops for a given day, and compare it against the fleet’s total capacity in a single loop (sum of all resources’ capacities).
- If total demand fits within total fleet capacity in one loop: model each delivery as a single
delivery stop. No pickup counterpart needed.
- If total demand exceeds total fleet capacity in one loop: a single-loop model is not just suboptimal, it’s infeasible — a real fraction of stops will land in
unaffectedStopIds regardless of how the rest of the plan is tuned. But don’t resolve this on your own by defaulting to the multi-trip pattern (or any other fix). A capacity shortfall is an operational question, not something you can decide from the data alone: it might mean the fleet genuinely reloads at a depot mid-shift (in which case Multi-trip tours is the right mechanism), or it might mean the client needs additional vehicles, a different schedule, or a split across multiple days. Surface the shortfall explicitly as an open question requiring client confirmation (see §1) — state the numbers (total demand vs. total capacity) and the possible resolutions, rather than picking one silently.
The aggregate check above is necessary but not sufficient — also run it per order: compare each individual order’s own demand against the largest single vehicle capacity available in the fleet (not the fleet’s total). An order whose demand exceeds every single vehicle’s capacity cannot be assigned as one stop under any fleet composition — the aggregate check alone won’t catch this, since a fleet with ample total capacity can still contain no single vehicle large enough for one oversized order. If you find one, split that order into multiple pickup+delivery legs (or otherwise decompose it) before finalizing the payload, rather than shipping it as a single stop and letting the solver silently drop it into unaffectedStopIds. Unlike the aggregate shortfall above, this check and its fix are fully derivable from data you already have (the order’s own demand, the fleet’s per-vehicle capacities) — it doesn’t require client confirmation, only that you actually run the comparison.
Do this arithmetic explicitly and state the result in your modeling notes, even when the answer is “fits comfortably, nothing to flag here.” This exception requires independent evidence, from the client’s own data or stated context, that the reload pattern is the intended operation (for example, it explicitly describes a depot with reload capacity or dock availability) — not merely a shortfall that happens to appear after you yourself estimated a missing figure (like fleet size) in the absence of real data. A shortfall you created via your own upstream assumption does not satisfy this precondition, no matter how well the resulting pattern happens to match reality — check that the depot/reload evidence exists in the source data on its own terms, independent of any number you had to guess to get there, before invoking this exception. If the client’s own data or stated context makes the reload pattern clearly the intended operation, you may model the multi-trip pattern and note it as a confirmed assumption rather than an open question — but the default when this isn’t stated is to ask, not to choose between single-loop and multi-trip yourself.
That independence check is necessary but not sufficient on its own: also confirm that the evidence itself has discriminating power. A flag or column cited as proof that an exception applies to a specific site, day, or subset must actually vary across the relevant data — a column that holds the exact same value on every row of the entire dataset, not just the filtered subset you’re looking at, cannot possibly indicate anything site- or day-specific, no matter how directly its name seems to match the question. Before citing any such flag as justification, check its distribution across the full dataset, not only within the rows you’ve already filtered down to — a column that’s constant fleet-wide has zero discriminating power, however plausible its name sounds for your specific case.
Before you let any aggregated total computed from the source data drive this decision, verify the actual semantics of every source column that feeds into it — check a handful of concrete rows and confirm whether a value scales with quantity the way a per-unit rate would, or whether it’s already an aggregated total. Don’t trust the column name alone. A unit mismatch or an accidental double-count can inflate a computed total by an order of magnitude, and a total that far off can itself trigger a structural pattern (for example, multi-trip) that the real data never called for — after which every decision that cascades from it (per-resource capacity, the reload pattern, etc.) will be wrong too.
3. Treat “contractual” and “hard constraint” as two different things
A time window described as contractual, SLA-based, or client-facing is not automatically a hard constraint. Read Hard vs soft constraints before deciding between authorizedTimeWindows and preferredTimeWindows for any window in your dataset.
If the source data doesn’t explicitly say whether missing a window means the stop becomes unserviceable (hard) or merely undesirable but still worth serving late (soft), don’t guess — this is a business decision, not a modeling detail. Default to preferredTimeWindows (the safer failure mode: a late stop still gets served) and list it explicitly as an open question for the client to confirm (see §1).
This default applies only when the data is genuinely silent — check for a positive signal before falling back to it. A column that literally states a site’s open/closed status, or any other explicit flag distinguishing “unserviceable outside this window” from “preferred, but still workable late,” is exactly the kind of stated fact the hard-vs-soft page’s own physically-closed-site example is describing — not an unstated case. When such a signal is present, it takes precedence over the soft default above: model it as authorizedTimeWindows (treating the signal as a stated hard constraint, not an assumption you invented) rather than defaulting to soft because the situation wasn’t phrased in exactly the words this checklist uses. The soft default exists for the case where no such signal exists at all, not as a fallback that overrides one once you’ve found it.
A generic “contractual doesn’t automatically mean hard” caution, on its own, is not a reason to override a positive site-level signal once you’ve found one. That caution exists to stop you from inventing hardness that isn’t stated anywhere — it has no work left to do once a literal status flag is already present in the data. If you find yourself reasoning from the general caution instead of engaging with a specific signal you already identified, that’s a sign you’re applying the caution outside the case it’s for, not a sign the signal doesn’t count.
4. Don’t guess on vehicle profile, break triggers, or other underspecified fields — check what the data actually supports
Some fields have more than one plausible value and no way to derive the “right” one from the API’s semantics alone:
vehicleProfile.type (car vs. truck, or any other vehicle-type/mode inference): if the source data doesn’t specify vehicle dimensions or gross weight, and doesn’t state a legal reason to require truck-specific routing, don’t assume one or the other from the vehicle’s commercial name alone — check Data model for what each profile actually changes, and flag the choice as an assumption if the docs don’t state a clear default for your case. The same rigor applies with at least as much force when a name or free-text label suggests a more exotic mode entirely (a bicycle, a motorcycle, and so on) rather than a choice between two similar road-vehicle profiles — inferring an exotic mode from a commercial name changes the routing mode/network even more drastically than car-vs-truck does, so it deserves at least as much scrutiny, never less, just because the name happens to look more specific or colorful. And if the source data contains a verified field that contradicts the name-based inference — for example, a declared capacity or gross weight that’s implausible for the literal vehicle type the name suggests — the verified field must override the name-based guess. Noting the contradiction as a caveat in your report while still shipping the name-based classification does not satisfy this rule; it’s the same disclosing-≠-complying failure as shipping a fabricated position while flagging it (see §5) — surfacing a conflict is not a substitute for resolving it in favor of the verified data.
breaks[] trigger type: a source dataset that gives you a break duration (e.g. “30 minutes”) without a triggering rule (a fixed time window, a cumulative working-time threshold, or a cumulative driving-time threshold) does not contain enough information to pick one of the three breaks[] types correctly. Don’t invent an arbitrary or unverifiable regulatory regime — but that’s a different case from applying a well-known, public regulatory default whose jurisdiction is unambiguous from data you already have (for example, a vehicle type together with a plan-level tz that together leave no real doubt about which country’s transport-sector break rules apply). In that narrower case, applying the well-known default is the required action, not merely a preference over shipping zero breaks: once the jurisdiction is genuinely unambiguous from data you already have, shipping zero breaks is a silent, unflagged omission like any other (see §7), not a neutral, safe fallback. Still list the applied default explicitly as an open question for client confirmation (see §1) rather than applying it silently — this doesn’t relax to a suggestion just because you’re required to act on it. A jurisdiction’s public default is frequently made of more than one concurrent rule rather than a single one — for example, French road-transport law imposes both a workingDurationSlidingBreak (a break after a cumulative amount of work) and a travelDurationSlidingBreak (a break after a cumulative amount of driving) at the same time, as two independent, non-substitutable entries in breaks[]. Applying only one of several rules a jurisdiction actually requires is a partial fix, not a complete one — it still leaves the same category of gap, just smaller and easier to overlook. The distinction that matters is arbitrary/unverifiable versus public and jurisdiction-obvious, not whether some default rule exists at all — only when you genuinely cannot establish the jurisdiction from data you already have does flagging-without-acting remain the right call.
Not every rule in a jurisdiction’s regulatory default carries the same confidence, though, and the duration figure is exactly where that distinction matters most: a rule set by public, jurisdiction-wide statute (independent of any single employer) is safe to apply with its exact statutory figure, while a rule that’s typically layered on top by a sector- or company-level collective agreement is not — a single number there is likely to be wrong for a specific client even when the type of break is obvious. For the EU/French road-transport travelDurationSlidingBreak case above, the driving-time break is public statute (EU Regulation 561/2006): minBreakDuration: "PT45M" after maxInterBreakDuration: "PT4H30M" of cumulative driving is safe to apply outright once the jurisdiction is established, same as any other required default in this section. Don’t extend that same confidence to the working-time rule’s duration in the same jurisdiction: its specific minute value is far more likely to be set by a collective agreement than by the statute alone, and unlike the driving-time case, there’s no generic figure the checklist can safely hand you — picking a plausible-looking number would be guessing at a fact this document has no way to know. minBreakDuration/maxInterBreakDuration aren’t optional on a breaks[] entry, so there’s no way to “include the rule without its number”: flagging that a workingDurationSlidingBreak entry is legally required is still the required action once the jurisdiction is obvious, but omit the entry itself from breaks[] rather than fabricate its duration to make one valid — state the omission explicitly in your open questions (see §1), alongside the break type and cadence it will need once the client confirms the figure, next to the driving-time entry you do default and ship.
- Objectives list, when the fleet composition is fixed rather than left free: if your
resources list is built by reproducing an existing fleet or a set of historical tours (rather than left open for the solver to size on its own), don’t just copy a default objectives list — explicitly check the full objectives catalog (see How the optimization engine works) for a tour-balancing objective (for example, one that caps how long the single longest tour can get relative to the rest of the fleet) and consider whether it applies. A fleet fixed from historical data is exactly the situation where individual tours can end up uneven — the scenario this kind of objective exists for — so re-check against the complete list rather than reusing whatever objectives a previous case happened to use.
5. Verify you have real geocoded positions before finalizing
Kardinal does not geocode addresses — every position you submit (resources’ departure/arrival, and every stop’s position) must already be a real {lat, lon} pair, precise enough to reflect the actual site. If the source data only gives you postal addresses, don’t fall back to an approximation (a town/commune centroid, a jittered placeholder) and consider the payload done: it will run and look structurally valid, but its results (distances, sequencing, time estimates) become meaningless, and stops in the same town collapse to the same point.
If you don’t have a way to geocode the addresses in your current environment, say so explicitly in your data-quality report (see §7) rather than silently shipping placeholder coordinates — a payload built on approximate positions looks complete while being unusable for a real optimization run.
Disclosing a substitute position as an open question does not satisfy this rule. Listing a fabricated coordinate (a statistical barycenter, a named landmark, a jittered point) in your open-questions list is not an alternative way to comply — it’s the same violation with a caveat attached, and it’s still built on a position that isn’t real. The required action for a missing or unreliable position is to leave the field unset (or otherwise not submit a concrete position for it) and flag that explicitly — never to ship any placeholder value alongside the disclosure, however prominently you disclose it.
This “say so, don’t ship a placeholder” instruction takes precedence over any general instruction to deliver a complete, immediately runnable payload — for this field specifically. If the two seem to conflict, the narrower, more specific stop-condition wins: an incomplete payload that honestly flags missing real positions is the correct deliverable here, not a complete-looking payload built on fabricated coordinates. This precedence rule isn’t specific to geocoding — wherever this checklist gives an explicit “stop and flag it” rule for a particular field, that rule overrides the general goal of payload completeness for that field, even when nothing in your task instructions explicitly carves out the exception.
6. Use the SDK’s typed objects when available; otherwise, validate against the data model field by field
If a Kardinal SDK is available for your target language, build Resource/Order/Stop objects through it rather than hand-writing a JSON dictionary — the SDK’s own docstrings carry modeling guidance at the exact point you need it, and it removes a class of structural mistakes entirely.
If you’re constructing the request body directly, cross-check every field you set against Data model before submitting — don’t rely on memory or on a single worked example to infer field names and types for a case that example didn’t cover.
7. Always deliver a modeling report and a data-quality report alongside the payload
A payload that “looks right” is not a safe deliverable on its own — whoever reviews your work (the client, or a colleague) needs to see what you decided and why, and needs to know if the source data itself has problems that limit what any agent or the API can do with it. Alongside the payload, always produce:
- A modeling report: every non-trivial decision you made — unit choices, field mappings, hard vs. soft trade-offs, assumptions about fleet or vehicle behavior — with a short justification each. This is broader than the open-questions list in §1: it covers the full set of choices someone would need in order to sanity-check the payload against their actual operation, not only the ones you were unsure about. This applies as a general self-check procedure to every claim you make about something your own code did, not just to the specific case that happens to be spelled out here: before writing a sentence like “this validates,” “this succeeds,” or “this check confirms X,” grep your own delivered script for the exact call or logic you’re describing, and quote the line number or a snippet as evidence. This is not a rule about validation-and-round-trip claims specifically (for example, re-parsing your own output through the SDK’s typed models to confirm it validates) — it’s a rule about any claim of this shape, on any topic, in any report you deliver. If you can’t point to the specific line that performs the claimed step, don’t make the claim, regardless of whether you recall running it in an earlier or interactive version of the script that isn’t part of what you’re shipping.
- A data-quality report: anything in the source data that blocks finalizing the modeling, or would produce an incorrect or incomplete result if silently worked around — missing values on fields you need, inconsistent codes (e.g. two spellings of the same location, a field that mixes two different coding schemes), contradictory rows, or values outside a plausible range. Don’t fix a data problem quietly and leave a one-line code comment as the only trace — list it explicitly so the client can correct it before the payload is used for anything real. The same traceability rule as above applies to any count or list you quote here (for example, “N addresses had low-confidence geocoding,” or “M rows had a missing field”): if your own script computes it, that number must be the script’s literal, reproducible output — copy it verbatim — not a hand-summarized approximation, a rounded-down estimate, or a partial list that quietly drops the worst instance. A summary statistic you can’t reproduce by re-running your own delivered script is the same unbacked-claim problem as an unverifiable validation claim, just applied to a number instead of a pass/fail.
A prose reminder to check your own delivered code before writing a verification or validation claim has repeatedly failed to survive contact with an actual delivery — this exact failure mode has now recurred across multiple independent studies, on different clients, in completely different business domains, and with different models each time, despite the rule already existing in prose immediately above. Don’t rely on prose alone a second time: whenever your modeling report lists verification or validation claims (any check that “confirms,” “validates,” or “ensures” something about the delivered payload or code), deliver them as a literal table — one row per claim, with columns for the claim itself and the exact file, line number, or function that implements it — not as prose sentences interspersed with your analysis. If your delivery format can’t render a table, use an equivalent fixed list of claim → file:line/function entries, one per line, in a consistent order. A table (or fixed list) has no room for a claim to appear without a matching implementation reference the way a paragraph does: a blank or missing cell is visible to a reviewer scanning row by row, where a plausible-sounding but unbacked claim buried in prose is not.
The traceability rule above is stated in terms of validation claims and summary statistics because those are the cases that have come up so far — but the underlying principle is broader than either: don’t assert anything, in any report, about a piece of state you have not checked at the moment you write the sentence. Two further cases of this same failure are common enough to call out by name, so that the rule doesn’t quietly narrow back down to only the cases already named:
- File-delivery claims. If your report says a file is included “for traceability,” “for reproducibility,” or any similar claim of presence, that file must actually exist in the folder you deliver — check with a literal directory listing right before you write the claim, not from memory of having created it earlier in your session. A claim that a file exists when it doesn’t is exactly as unbacked as a validation claim with no matching code, even if every number that file would have contained is itself accurate and reproducible elsewhere.
- Claims about the current state of Kardinal’s own documentation or API. If your report characterizes what the docs or spec currently say (for example, that a field’s description is still a placeholder, or that a page states a particular default), that claim must come from reading the live file in front of you at the time you write it — not from a prior session, a cached impression, or something you recall reading in a different context. Documentation changes between sessions; a claim about its current content is a claim about state exactly like a validation claim is, and needs the same fresh, direct check before you write it down.
Without these two reports, no one downstream can tell an informed decision from a guess, or a clean dataset from one with problems baked in.
8. Derive geographic scope from an authoritative reference table, not a postal-code heuristic, when one exists
When a task asks you to scope or filter entities (customers, sites, orders) to a specific geographic region — a metro area, a delivery zone, a sales territory — don’t reach for a postal-code-prefix heuristic (matching codes that start with a given digit sequence) as your only method. Check first whether the source data already contains an authoritative reference table with real coordinates for the relevant entities (a depot, agency, or site table, for instance). If one exists, prefer deriving region membership from that table — for example, assigning each entity to its nearest depot/agency by actual distance — over a postal-code-prefix match.
A postal-code prefix is an administrative proxy for geography, not geography itself: postal boundaries don’t reliably align with operational or metro-area boundaries, and a prefix match will silently misclassify boundary cases — an entity whose postal code happens to fall just outside (or inside) your chosen prefix range even though it’s operationally served by, or physically closest to, a site inside (or outside) your intended scope. This exact mistake has recurred across more than one client dataset, with different agents making it independently on different runs, and in every case the correct classification was recoverable purely from data already in hand (real coordinates on an existing reference table) rather than from anything requiring new client input — so this is an avoidable execution gap, not an irreducible client-only ambiguity to flag and move past.
Apply this generically, regardless of client or dataset: whenever a depot/agency/site reference table with coordinates exists in the source data, use nearest-entity matching (or another distance-based method against that table) as the primary way to determine region membership, and treat a postal-code prefix as a fallback only for the entities or datasets where no such coordinate-bearing reference table exists at all.
A related check applies even earlier, before you reach the scoping step above: when a task hands you a client-supplied perimeter or scope code (a site code, a zone code, a region code) to filter on, check whether that exact code string is reused at a different level of granularity elsewhere in the source data — for example, an operational code that identifies one specific site in the sheets that actually build your resources and orders, but that happens to share its exact value with a broader administrative or regional grouping label in a separate, purely descriptive sheet. When the columns that actually construct your resources and orders use the code at one specific granularity consistently, prefer that operational-level reading over a broader one found only in a descriptive or administrative sheet — the sheets doing the real construction work are the stronger signal. If there’s genuine ambiguity about which granularity was intended, don’t resolve it silently: surface it as an open question requiring client confirmation (see §1), the same as any other irreducible ambiguity.
See also