Config, DSL, or code: structuring dozens of third-party integrations

You support ten third-party services today and the roadmap says fifty. Each provider has its own setup dance: create a resource, wait for it to become ready, mint a credential, register a callback. An agent or automation layer has to execute every one of them reliably, without an engineer watching. Before you write the first integration, you have to decide how integrations are expressed — hardcoded modules, model-generated steps, a declarative DSL, or sandboxed code behind a fixed contract. That choice determines your maintenance cost, your review story, and whether anyone outside the core team can ever add a provider.

I have direct evidence on this decision. At ZeroClick I originated and led an agent auto-provisioning system where each supported third-party service had an integration definition an agent could execute — provisioning real accounts and credentials on real providers. We demoed it to partners and advertisers, and it shaped the company’s subsequent product direction. Before the design that held up, we built and abandoned two others. The dead ends are documented, and they map onto the options every team weighs.

Option 1: hardcode each integration

A module per provider, written by your engineers, living in your repo. This is the right answer more often than architecture discussions admit. If you have five providers, they change rarely, and engineers on your team own all of them, hardcoding gives you full language power, normal code review, normal tests, and zero novel infrastructure.

It stops working on two axes. Count: at dozens of providers, every addition is an engineering ticket, and integration work crowds out product work. Authorship: the people who know a provider’s setup quirks are often outside your team — a partner’s engineers, a solutions team, an operator — and a hardcoded model gives them no way in short of a pull request to your codebase.

Option 2: generate the integration at call time

The tempting version: providers already publish setup documentation, so let a model read the docs and produce the integration when a request arrives. No integration catalog to maintain at all.

We tried this — a retrieval pipeline over provider documentation, with the model generating the integration on the fly — and killed it. The documented reasons: non-determinism, meaning the same request produces different steps on different runs, and cost, because every execution pays for model calls and their latency. The deeper problem generalizes past our case. There is no artifact. Nothing to review, nothing to diff, nothing to point to when a provisioning run misbehaves at 2 a.m. You have substituted an unreviewable step for a reviewable one, in the execution path, against third-party systems that create real resources.

The salvageable part is the authoring speed. Generate a candidate integration from the provider’s docs at authoring time, have a human review it, freeze it, and execute the frozen artifact in production. The model accelerates the writing; it stays out of the running.

Option 3: a declarative step DSL

The second attempt was declarative steps with variable interpolation — executable instructions with tool hints and ${var} substitution, run by an executor. It handled the first few integrations well. Simple sequences of API calls with values threaded between them fit a step list.

Then a real provider arrives. One of our integrations — Supabase is the illustrative case — ran a couple hundred lines: poll the provider until the project reports a healthy status, generate a cryptographically random database password, enumerate the account’s organizations, then look up API keys by name after creation. A step list expresses none of that. So you add a polling keyword. Then a conditional keyword. Then retries, then generated values. Each addition is reasonable alone, and the sum is a programming language — one with no debugger, no ecosystem, and tooling only your team maintains. We killed the DSL for exactly this: real provisioning needs loops, conditionals, and polling that a step format can’t express.

Option 4: split by what is actually standardized

The design that held up starts from an observation about the problem rather than a preference about representation. Authentication is standardized across providers. OAuth flows, API keys, token endpoints — the variation fits a schema. The setup dance is not standardized. Every provider’s is different, and the differences are structural.

So the integration definition split in two. Authentication became declarative configuration: the auth mode, authorize and token endpoints, client credentials, scopes, PKCE, and how the token request is authenticated. The setup logic stayed code — plain JavaScript, executed server-side in a sandbox, behind a fixed lifecycle contract. Every integration implements the same four hooks: provision the resource, hand it over to the user, follow up after hand-over, and tear it down. The hooks close over a small injected API, and that is the entire interface.

The uniformity is what pays. The platform can run, monitor, retry, and expire any integration without knowing what it does, because the lifecycle shape is identical across all of them. The code inside a hook can be as gnarly as the provider demands — the two-hundred-line polling loop lives comfortably in a hook — and none of that gnarl leaks into platform machinery.

The operational consequences

The representation choice is the visible decision. What made the system workable day to day was a set of practices around it.

  • Keep the injected surface narrow, and treat it as the contract. Our hooks received a token, a context object, a request function, and a way to ask the user a question mid-flow. That list is the whole capability grant. Everything the integration can do, it does through those primitives, which is what makes the code sandboxable, auditable, and portable across integrations.
  • Build an authoring surface. Code-as-integration only opens authorship beyond your team if authors get tooling. We built an internal tool with a code editor per lifecycle hook, a preview that renders exactly what production renders — same encoding, byte for byte — and a test harness that runs the full lifecycle including the human-input step. Without this, option 4 quietly collapses back into option 1.
  • Degrade gracefully. An integration that is missing, incomplete, or failing to load should fall back to the ordinary experience rather than erroring. Our system had three separate fallback levels — definition absent, metadata fetch failed, required fields incomplete — and every one of them rendered the normal path. Users saw a working product; only the enhancement disappeared.
  • Version and review integration code like code. Because it is code, and it runs on your infrastructure. It gets diffs, review, and an audit trail. This is the concrete advantage over option 2, and it only exists if your process treats the artifacts accordingly.
  • Take the security posture seriously.Executing integration code written by partners on your servers is a distinct threat model, and the sandbox boundary — what the injected request function may reach, what the code may log, what credentials it may see — demands its own controls. That is a separate article’s worth of lessons.

The decision in one pass: hardcode while the count is small and your team owns every provider. Never generate in the execution path; generate at authoring time and freeze. Reserve declarative formats for the parts of the problem that are genuinely uniform, like authentication. For the setup logic itself, use real code in a sandbox behind a fixed lifecycle contract, and invest in the authoring, fallback, and review practices that make it operable.

Questions clients ask

Should I let an LLM write my integrations at runtime?

No. Runtime generation puts a non-deterministic step in your execution path: the same request can produce different steps, you pay model cost and latency on every call, and there is no artifact to review or diff. Use the model at authoring time instead — generate a candidate integration from the provider’s docs, have a human review it, and freeze it. You keep the authoring speed and lose the runtime risk.

When is a declarative DSL the right answer?

When the thing you are describing is genuinely uniform across providers. Authentication is a good example: mode, endpoints, credentials, scopes, and PKCE cover nearly every provider, so config works. Setup workflows are not uniform — real ones need polling loops, conditionals, generated credentials, and retries — and a DSL that grows those features becomes a programming language with worse tooling. Split the problem: config for the standardized part, code for the rest.

How do non-engineers add an integration in a code-based model?

Through an authoring surface, not a repo checkout. An internal tool with a code editor per lifecycle hook, a preview that renders exactly what production renders, and a test harness that runs the full lifecycle lets a technical operator or a partner’s engineer author an integration without touching your codebase. The fixed contract — a handful of injected primitives and named lifecycle hooks — is what keeps their code inside guardrails.

How do I test integrations against real providers?

Run the full lifecycle, including the human step. An integration that provisions a resource, waits for it to become healthy, asks the user a question mid-flow, and tears everything down afterward has failure modes a unit test will not reach. Build a harness that executes provision, hand-over, follow-up, and teardown end to end against a sandbox or test account, and make the interactive prompt part of the test run rather than something you stub out.

Deciding how your agent layer should execute integrations?

The agent-readiness audit is $3,000, fixed. I evaluate your integration architecture, your codebase, and your workflow through an agent's eyes, run a live pilot on your actual code, and hand you a sequenced plan. The fee is credited toward any follow-on work.

Book a free intro callEmail me insteadI personally reply within one business day. · Currently booking September 2026.