Procore

94% self-serve year one

Imports 4xx error contract. Field-level declarations replacing cryptic failure states. 6 weeks to 3 days time-to-data.

Two parallel API planes (preflight and commit) returning identical typed error contracts. Visual metaphor for the dry-run as part of the public surface.

Enterprise customers were stalling at onboarding because every import was hand-run by a white-glove team, and white-glove didn't scale. The hardest part wasn't moving data. It was making customers self-sufficient when their CSV columns never matched Procore's schema and a failed first import told them nothing useful.

Read the case What shipped, how it worked, the lines that land
A typed key fitting four possible doors. Visual metaphor for the field-level declared error contract that opens self-serve across multiple verticals.
One typed key, four possible doors. Field-level declared rejection across eight verticals.

Role and arc

Stepped in mid-flight as lead on Procore Imports after the prior owner left. Took it from a Wizard-of-Oz Excel macro to a generally available self-serve enterprise data ingestion platform. Career arc at Procore: Product Manager Resident to Associate PM to PM (2017 to 2020), Customer Success to Mobile Platform to Mobile Financials to Imports.

What I shipped

A declared error contract at the field level

A Postman-validated public Imports API where every rejected row told the customer which field, what value, and what to change:

{ "field": "trade_id", "code": "unknown_value" }

The contract forced API and import to behave identically at every public boundary, written into a spec. Cryptic failure states (the customer's #1 support driver) became declared, addressable signals.

A parametric PostgreSQL schema

One schema that flexed across 8+ Procore verticals (financials, projects, schedules, vendor lists, trade data, project metadata, and more) without per-vertical forks.

A fake-API preflight rate-limit coordination layer

So customers could validate ingestion shape before committing real bandwidth.

Inclusive dev with support

Early-influencer buy-in, roundtables, transparent training. Converted the white-glove team from threatened-by into co-owners of the self-serve platform.

Results

  • 94% adoption Year 1, 100% Year 2.
  • Onboarding compressed from 6 weeks to 3 days.
  • Roughly $1.5M in annual savings.
  • Import-related support tickets collapsed; white-glove capacity redirected to higher-leverage work.
  • 8+ verticals supported on the parametric schema.

Adjacent Procore receipt

Mobile Financials. Discovered an anomaly in mobile metrics (enterprise customers hitting financial product domains from mobile browsers), converted a latent demand signal into a new product category. Mobile Financials retention: 19.1% to 30%, +10% weekly active users. Same pattern as Imports: declare what the data is actually saying, then build the surface for it.

Craft: how it actually worked

Contract

4xx, byte by byte

Every rejected row returned the same { field, code } shape. The exact field that failed and a typed code that maps to a known remediation. Clients render their own copy from the code. The contract bound API and import to identical rejection semantics at every public boundary.

Schema

One shape, eight verticals

Financials, schedules, vendors, trades, project metadata, RFIs, drawings, submittals. Each a parameterization of the same backbone instead of a per-vertical fork. New verticals added rows in a config table; they did not add migrations.

Preflight

Two planes, one contract

A rate-limit-coordinated dry-run endpoint so customers could shape-validate before committing real bandwidth. Removed the "import failed at row 9,847 after eleven minutes" failure mode. The dry-run plane is part of the contract; it returns the same 4xx shape as the real plane.

Org change

White-glove to co-author

Early-influencer buy-in. Roundtables. Transparent training. Joint authorship of error copy. The team that could have been threatened by self-serve became co-owners of it, then redeployed to higher-leverage work once their old work was automated.

The system tells the user what's true, instead of making them infer.
See the proof Composition, primitives, first principles
Cryptic failure states are an inference tax. Declared failure states are a contract.

Primitives: the reusable architecture

Field-level declared rejection

Every "no" carries the exact field and the typed code. Reusable across any ingestion surface, any vertical, any client SDK. The primitive scales horizontally. The same {field, code} shape works for CSV imports, public API calls, and mobile-app form submissions.

Parametric schema

Vertical-flex via shape parameters, not per-vertical forks. Same primitive Oracle's customer ontology uses at platform scale. Forks decay; parameters compose.

Preflight as a separate plane

Validate shape on a fake plane. Commit on the real plane. Both planes return the same contract. The fake plane is not an afterthought. It is part of the public surface.

How this composes

  • Oracle Typed declared events at platform scale.
  • Amazon Same banded shape at workforce-survey scale. The field-level error contract is the rather-not-answer affordance at the API boundary.
  • Clause Proceed, proceed-with-warnings, block. Same banded shape.
  • Declaration Declared scope is what bounds the system's response.
Self-serve at scale is a measurement problem disguised as an ingestion problem.

First principles

The system tells the user what's true

Cryptic failure states are an inference tax. The user is forced to guess what the system meant. Declared failure states are a contract. The user is told. Every byte of inference the user has to perform is a byte the system should have surfaced.

Self-serve at scale is a measurement problem disguised as an ingestion problem

"Why did my import fail?" is the actual product. Until the system answers field by field, every customer is a support ticket. The 4xx contract is what makes self-serve actually self-serve.

The contract is the API

Spec-level. No verbal agreements between teams about what 422 means. The contract is enforced by tests, written in the public docs, and identical across every boundary the surface exposes.

Why this matters for Anthropic

Tool-calling and agent feedback need exactly this. When a model receives a 4xx from a tool, { field: "trade_id", code: "unknown_value" } is actionable; "request failed" is not. Field-level declared errors are the substrate agentic feedback loops require to self-correct.

The contract is the API. No verbal agreements between teams about what 422 means.