All posts
Engineering

AI Agents for API Contract Tests: Catching Breaking Changes Before They Merge

DevOS Platform TeamJuly 3, 202613 min read

Tuesday, 4:15 PM. You merge a "simple" backend change — removing a deprecated field from the /users endpoint. Tests pass. CI green. Ship it.

Wednesday, 9:00 AM. Three Slack channels are on fire. The mobile app's profile screen is blank. The partner integration is returning 500s. The internal dashboard hasn't loaded for anyone since midnight.

You removed display_name. The mobile app expected display_name. Nobody told you the mobile app expected display_name because nobody knew except the mobile team, and they're in a different timezone.

I've done this. More than once. (The second time was worse because I should've learned the first time.) The problem isn't carelessness — it's that you can't know what you don't know. Your service doesn't know who's calling it or what they expect. Your tests verify your code works, not that your API contract is preserved across every consumer.

Contract tests exist to solve this. Consumer-driven contracts, Pact, OpenAPI validation — the tooling exists. But writing and maintaining contract tests manually? That takes time nobody budgets. So the contracts drift. The tests rot. And you ship that Tuesday merge.

What if an AI agent just... handled this? Scanned your consumers, generated the contracts, updated them when usage patterns changed, and blocked PRs that broke them. Like a QA engineer who's read every HTTP client call in every consumer repo and remembers all of them.

That's what we're building with DevOS. The contract testing workflow where agents do the grunt work and humans handle the judgment calls. Here's how it actually works.

What We're Building

By the end of this, you'll understand the full contract test workflow with AI agents:

  1. Agent scans consumer services to identify API dependencies
  2. Agent generates contract tests based on actual usage (not just OpenAPI docs)
  3. Agent runs contracts on every producer PR
  4. Breaking changes get flagged before merge
  5. Agent updates contracts when consumers change their expectations
  6. Humans review and approve contract updates — agents propose, humans decide

The result: cross-service breakage gets caught on the PR, not in production. Your 4:15 PM merge doesn't become your 9:00 AM incident.

Prerequisites

Before this workflow makes sense:

  • At least two services that communicate over HTTP (or GraphQL, or gRPC)
  • A version control setup where producer and consumer live in accessible repos
  • CI that runs on PRs (GitHub Actions, GitLab CI, whatever — the agent needs a hook)
  • Some API documentation — OpenAPI spec, GraphQL schema, or at minimum consistent response shapes
  • Basic familiarity with the concept of contract testing (even if you've never implemented it)
  • Analytics tracking with JustAnalytics to measure how many breaking changes the workflow catches (optional but satisfying)

If your services are monolith-only, this post isn't for you. Contract testing matters when you have service boundaries. One service, no contracts needed.

Step 1: Agent Scans Consumer Services for API Dependencies

The agent needs to know who calls your API and what they expect. Manual documentation is always stale. ("The wiki says three services consume this endpoint. The wiki is from 2024. Seven services consume it now." I've had this exact conversation.)

Instead, the agent scans:

HTTP client calls in consumer codebases:

# The agent finds this in consumer-service-a
response = requests.get(f"{USER_SERVICE_URL}/users/{user_id}")
user_data = response.json()
display_name = user_data["display_name"]  # <-- consumer expects this field
email = user_data["email"]

TypeScript interface definitions:

// The agent finds this in consumer-service-b
interface UserResponse {
  id: string;
  display_name: string;  // <-- consumer expects this field
  email: string;
  created_at: string;
}

GraphQL queries:

# The agent finds this in consumer-service-c
query GetUser($id: ID!) {
  user(id: $id) {
    displayName  # <-- consumer expects this field
    email
  }
}

The agent builds a dependency map: "Service A calls /users/{id} and expects fields display_name, email. Service B calls the same endpoint and expects id, display_name, email, created_at."

Here's where you see the ugly truth. Your OpenAPI spec might say display_name is deprecated. But Service A still uses it. Until you actually scan the consumers, you're flying blind. (I spent two weeks once maintaining a "deprecated" field that exactly zero consumers had stopped using. Two weeks of my life I'll never get back.)

Step 2: Agent Generates Contract Tests

Now the agent writes tests. Not integration tests — contract tests. They verify the shape of data, not business logic.

For each consumer-producer relationship, the agent generates:

# Auto-generated contract test for consumer-service-a
# Validates /users/{id} response against consumer-service-a's expectations

def test_users_endpoint_contract_for_consumer_service_a():
    """
    Contract: consumer-service-a expects display_name, email
    Generated from: consumer-service-a/src/clients/user_client.py:47
    """
    response = producer_client.get("/users/test-user-123")

    assert response.status_code == 200
    data = response.json()

    # Fields consumer-service-a uses
    assert "display_name" in data, "consumer-service-a expects display_name"
    assert "email" in data, "consumer-service-a expects email"

    # Type checks
    assert isinstance(data["display_name"], str)
    assert isinstance(data["email"], str)

The test doesn't verify that display_name is "John Doe." It verifies that display_name exists and is a string. That's the contract.

The agent generates one test per consumer per endpoint. If three consumers call /users, you get three contract tests. They might overlap (all expect email), but they're separate because each consumer has its own expectations. Consumer A might only use two fields; Consumer C might use five.

This matters when you want to deprecate a field. You can see exactly which consumers still depend on it.

Step 3: Agent Runs Contracts on Every Producer PR

Here's where it gets good. (Finally, something that actually works.)

When a developer opens a PR that modifies /users:

  1. CI triggers the contract test suite
  2. The agent runs every consumer contract against the proposed changes
  3. Any violation fails the build and blocks merge

Example failure:

FAILED test_users_endpoint_contract_for_consumer_service_a

AssertionError: consumer-service-a expects display_name

The field 'display_name' was removed in this PR but consumer-service-a
still depends on it.

Consumer usage: consumer-service-a/src/clients/user_client.py:47
  > display_name = user_data["display_name"]

Options:
  1. Revert the removal (unintentional breaking change)
  2. Coordinate with consumer-service-a team to update their code
  3. Add display_name back as deprecated with removal timeline

The PR doesn't merge until either: (a) the field comes back, or (b) someone explicitly acknowledges the break and plans the consumer updates. No silent breakage. No Wednesday morning surprises.

Is this annoying? Honestly, sometimes. You'll hit contract failures for changes you know are fine, and you'll grumble about it. But I'll take a few false positives over another 9 AM production fire any day.

In DevOS's planned CI integration, this runs automatically via GitHub Actions. The agent adds a status check to every PR touching API endpoints. Red until contracts pass.

Step 4: Agent Detects Intentional vs. Accidental Breaks

Not every contract failure is a bug. Sometimes you genuinely need to break compatibility — you're removing a deprecated field, changing a type, restructuring a response.

The agent helps here too. When a contract fails, it analyzes the change:

Accidental break signals:

  • Field removed without deprecation notice
  • Type changed (string → number) without migration path
  • No mention of breaking change in PR description

Intentional break signals:

  • Field was already marked deprecated in OpenAPI spec
  • PR description mentions "breaking change" or "migration"
  • Consumer team members are reviewers on the PR

For intentional breaks, the agent suggests a migration plan:

Breaking change detected: removing 'display_name' from /users

This field is used by:
  - consumer-service-a (production)
  - consumer-service-b (production)
  - internal-dashboard (internal)

Suggested migration:
  1. Add 'display_name' as alias for 'name' (backward compatible)
  2. Update consumers to use 'name' instead
  3. Mark 'display_name' as deprecated with removal date
  4. Remove after all consumers updated

Shall I create tickets for consumer updates?

If you confirm, the agent creates tickets in each consumer team's backlog. The migration is tracked.

(Okay, fewer things fall through cracks. Some things always fall through cracks. I'm not promising perfection here — I've been burned too many times for that.)

Step 5: Agent Maintains Contracts When Consumers Change

Contracts aren't write-once. Consumers evolve. A consumer that only used email might start using display_name. A new consumer might appear.

The agent runs periodic scans (configurable — daily, weekly, on-consumer-PR) and updates the contracts:

Contract update detected for /users/{id}

consumer-service-d (NEW)
  Added dependency: wants fields id, email, phone

consumer-service-a
  Removed: no longer uses display_name (was deprecated)

Proposed contract changes:
  + Add contract test for consumer-service-d
  - Remove display_name assertion from consumer-service-a contract

[Approve] [Review Changes] [Dismiss]

You review and approve. The agent doesn't silently change contracts — that would defeat the purpose. But it handles the tedious discovery work so you don't have to remember to check. Look, I'm not great at remembering to check things. Most of us aren't. That's kind of the point.

Common Errors and How to Fix Them

"Agent couldn't find consumer dependencies"

The agent scans for HTTP client patterns it recognizes. If your consumer uses an unusual client library or wraps calls in heavy abstraction, the agent might miss them.

Fix: Add a .contract-hints file in the consumer repo listing API dependencies explicitly. Or configure the agent to scan additional patterns specific to your codebase.

"Contract test passes locally but fails in CI"

Usually an environment mismatch. The contract tests run against a mock producer in CI but real responses locally.

Fix: Ensure your CI contract tests use recorded fixtures that match actual producer responses. The agent can record these from staging if configured. Tools like JustBrowser for browser automation can help capture and replay API responses in automation scenarios.

"Too many contract tests — builds are slow"

Each consumer × endpoint = one test. With many consumers and many endpoints, this adds up.

Fix: Run contract tests only on PRs that modify API endpoints (path-based triggering). The agent can detect which endpoints a PR touches and run only relevant contracts. Full suite runs nightly or on release branches.

"Contract says field is required but OpenAPI says optional"

The contract reflects actual consumer usage, not documentation. If consumers treat an optional field as required (by always expecting it), the contract reflects that reality.

Fix: This is working as intended — the contract protects real consumer behavior. Your docs say one thing; your consumers do another. Guess which one matters at 3 AM? If you want to make a field truly optional, update consumers to handle its absence, then regenerate contracts.

Connecting This to Your CI/CD Pipeline

Contract tests fit into the existing CI flow. They're not a separate system — they're a check that runs alongside unit tests and integration tests.

The typical setup:

# .github/workflows/pr-checks.yml
jobs:
  contract-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run contract tests
        run: |
          pytest tests/contracts/ -v
      - name: Report contract status
        if: failure()
        run: |
          # Agent posts summary to PR comments

In DevOS's approach, the agent manages this workflow end-to-end. It generates the tests, maintains them, runs them in CI, and reports results. The YAML above is simplified — the actual integration handles more (contract recording, consumer discovery, migration suggestions). But the concept is the same: contract tests are a CI check that blocks breaking changes.

Tracking which PRs get blocked and why helps measure the value. Analytics dashboards from JustAnalytics that show "breaking changes caught per week" prove the workflow is worth the hassle. And "time since last production contract breach" makes a satisfying sprint retro metric — assuming you can actually go more than a week. (We couldn't, at first. It was humbling.)

Next Steps

Once contract testing is working:

Add versioning: Instead of blocking all breaks, the agent can detect whether a change needs a version bump. /v1/users contract breaks? Suggest creating /v2/users and deprecating v1. Learn more about context engineering for large codebases — API versioning strategies pair naturally with AI agent API contract testing workflows.

Extend to event contracts: If services communicate via events (Kafka, SQS, whatever), the same pattern applies. Event schemas are contracts. Producer changes event shape; consumer contract tests fail. The agent scans event handlers the same way it scans HTTP clients. (Event contracts are actually harder to debug when they break, in my experience. At least HTTP gives you a status code. Events just... disappear into the void.)

Generate SDKs from contracts: If your contracts are precise enough, the agent can generate typed client SDKs for consumers. Consumer-service-a gets a generated UserClient with typed methods. Drift becomes impossible because the SDK is generated from the source of truth.

If you're managing multiple services and tired of cross-service breakage — DevOS is pre-launch, but the waitlist is open. Contract testing is one of the workflows where agents make the most sense. Structured inputs, clear outputs, repetitive execution. Agents handle the grind; humans make the calls. For teams building SaaS products, our experience building 9 SaaS products showed that AI agent API contract testing prevents the majority of production incidents.

Frequently Asked Questions

How does an AI agent know what contract tests to write?

The agent analyzes your API's OpenAPI spec (or similar schema), identifies consumer services by scanning import statements and HTTP client calls, and generates contract tests that validate the producer's response shape matches what consumers expect. It reads actual usage patterns, not just documentation.

Can AI agents detect breaking changes before PRs merge?

Yes — that's the main point. When a PR modifies an API endpoint, the agent runs existing contract tests against the proposed changes. If a field is removed, a type changes, or a required parameter is added, the contract tests fail and block the merge. The agent can also suggest whether the break is intentional (needs consumer updates) or accidental (revert the change).

What's the difference between contract tests and regular integration tests?

Integration tests verify that services work together in a deployed environment. Contract tests verify that the shape of data exchanged between services matches expectations — without needing both services running. They're faster, more targeted, and catch interface mismatches earlier. Think of them as type checking across service boundaries.

Does this work for GraphQL or only REST APIs?

Contract testing works for both. REST contracts validate JSON response shapes against OpenAPI or JSON Schema. GraphQL contracts validate that queries return the expected types and fields. The approach is the same — the agent just uses different tooling (GraphQL schemas instead of OpenAPI). gRPC works too, using Protocol Buffer definitions as the contract.


Join the DevOS Waitlist

AI agents that work as employees inside your sprints, standups, and tickets — not single-task copilots. Planner / Developer / QA / DevOps agents pick up work from the backlog, ship in branches, request review. Linear-shaped backlog UI with AI underneath. Pre-launch.

Join the waitlist → · How agents-as-employees works