Composition

What a Function Is, and Why It Outlives a Redeploy

A Function is w6w's vendor-abstracted single operation: a stable interface that stays put while its implementation changes underneath it.

Published .

compositionwhen one call isn't enoughFunctionEndpointWorkflowplug and play

What a Function Is, and Why It Outlives a Redeploy

I’ve built the same integration three times, for three different products, and each time it was wired straight to one vendor’s shape: this app’s action name, that app’s parameter keys. The day product two needed a different provider, each caller that touched it had to change too — not because the idea of “send a receipt” changed, just the vendor underneath it. That’s the actual problem, and it’s a bigger idea than the one demo everybody remembers it by.

So what is a Function, exactly?

A Function is a named, reusable, vendor-abstracted single operation — “Send Email,” “Create Contact,” “Charge Card” — that declares a stable interface and binds it to one concrete app action through a swappable implementation. Callers bind to the interface: inputs and output. They never bind to the vendor. Switching the provider underneath replaces only the implementation block and never breaks a caller. A Function is composition, not a new engine — it wraps the same single-action invoke path each ad-hoc call already goes through, so nothing about credentials, sandboxing or metering changes just because a Function is in front of it.

It sits one level above a raw app action, and it can be used two ways: invoked directly, once, or as one step inside a Workflow. When one call isn’t enough, a Function is usually the unit you reach for before you reach for a whole graph of steps — a lot of “integrations” are one Function away from being one Function, not a workflow at all.

The proof: swapping SendGrid for Brevo without a redeploy

Here’s the Function definition before, wired to SendGrid — impl.uses names the app and action, and inputs/output/id/key live outside this block entirely, untouched by a swap:

"impl": {
  "uses": { "app": "io.w6w.sendgrid", "action": "send-mail", "connection": "conn_02K…" },
  "with": {
    "personalizations": [{ "to": [{ "email": { "$": "inputs.to" } }] }],
    "subject": { "$": "inputs.subject" },
    "content": [{ "type": "text/html", "value": { "$": "inputs.body" } }]
  },
  "outputMap": { "messageId": { "$": "output.headers.x-message-id" } }
}

And after — the only block that changed. Every caller, direct or inside a Workflow, is untouched:

"impl": {
  "uses": { "app": "io.w6w.brevo", "action": "send-email", "connection": "conn_01H…" },
  "with": {
    "recipientEmail": { "$": "inputs.to" },
    "subjectLine":    { "$": "inputs.subject" },
    "htmlContent":    { "$": "inputs.body" }
  },
  "outputMap": { "messageId": { "$": "output.id" } }
}

inputs, output, id and key are identical across both blocks. A host that implements Functions is required to reject a swap that would change any of them, precisely so a provider migration can never quietly break an existing caller. That’s the whole vendor-swap invariant, and swapping SendGrid for Brevo is one worked example of it — not the reason a Function exists.

This won’t help you if your vendor’s API isn’t shaped anything like the one you’re leaving — mapping a genuinely different data model through with and outputMap is real work, each time. What it removes is the second cost: each caller that has to change too, on each migration, forever.

What else changes without a redeploy?

Swapping an impl is the demo. It’s not the whole pillar. The same “change what’s underneath without shipping code” idea also covers the parts of an integration that are not vendor bindings at all: project variables, secrets and documents. A rate limit, a feature flag, a template id, an API base URL — the values a running integration reads, not the code path it runs — are editable the same way, without a deploy, without touching the caller.

And because everything an app ships resolves through a pinned content-addressed registry, a tenant can pin a specific version of an app and roll it forward deliberately, instead of inheriting whatever a publisher pushed this morning. That’s the enterprise half of plug and play: not just “the vendor changed,” but “we control exactly when anything underneath us changes at all.”

Where does this sit next to a Workflow, and an Endpoint?

A Function gives you one stable operation. A Workflow composes several of them — and raw app actions — into a DAG when one call genuinely isn’t enough. A Workflow step can target a Function directly, the same way a direct caller can, so promoting three Function calls into one orchestrated run doesn’t mean rewriting any of them.

An Endpoint sits one layer above both. It’s a named, stable entry point a caller binds to once, and it can point at a Function today and a Workflow tomorrow without the caller ever knowing the target changed shape. That’s a second kind of stability, on top of the Function’s own: the interface survives a vendor swap, and the entry point survives being promoted from a single operation to a whole graph. Neither layer asks the caller to know which one it’s talking to.

The maintenance tax this actually removes

I’ve built this integration by hand twice before, and both times the maintenance cost more than the build did. A Function doesn’t make integration work disappear — the SendGrid-to-Brevo mapping above is still real work. What it removes is each caller having to redo that work alongside the one person who’s actually doing the migration, which was never the interesting part of the job to begin with.

← Back to all articles