4.2.1. Triggers and Bindings for Serverless APIs
💡 First Principle: Bindings are declarations of data flow, not code: you state "this function runs on queue messages, reads this Cosmos DB document, writes to that container," and the runtime performs the plumbing. Less SDK code means less to secure, test, and debug — the whole value proposition in one design choice.
In the Python v2 programming model, triggers and bindings are decorators:
import azure.functions as func
app = func.FunctionApp()
@app.service_bus_queue_trigger(arg_name="msg", queue_name="embed-jobs",
connection="SB_CONN")
@app.cosmos_db_input(arg_name="doc", database_name="chatdb",
container_name="documents", id="{msg.docId}",
partition_key="{msg.tenant}", connection="COSMOS_CONN")
@app.cosmos_db_output(arg_name="out", database_name="chatdb",
container_name="embeddings", connection="COSMOS_CONN")
def embed_document(msg: func.ServiceBusMessage, doc: func.Document,
out: func.Out[func.Document]):
vector = embed(doc["content"]) # the only real logic
out.set(func.Document.from_dict({"id": doc["id"], "embedding": vector}))
One Service Bus trigger starts execution; a Cosmos DB input binding fetches the referenced document (note the {msg.docId} binding expression pulling values from the trigger payload); an output binding persists the result. The function body is pure logic — no clients, no connection management. The trigger menu maps one-to-one onto this guide's services: HTTP (serverless APIs), Service Bus, Event Grid, Cosmos DB change feed (a managed change-feed processor, connecting to 2.1.4), timer, and blob.
For serverless APIs specifically, the HTTP trigger plus route parameters (@app.route(route="chat/{sessionId}")) builds REST endpoints; auth levels (anonymous/function/admin) gate access with keys, though production APIs typically front Functions with API Management or Entra ID.
Connections in binding decorators (connection="SB_CONN") name app settings, not literal strings — configuration stays external per the 3.1.3 doctrine, and can resolve via Key Vault references or managed identity.
⚠️ Exam Trap: Scenario code calling the Cosmos SDK inside a function to fetch one document by ID — when an input binding with a binding expression would do — is the "simplify this" question: replace SDK calls with bindings. Conversely, bindings can't express complex queries or conditional writes; needing those is the legitimate reason to use the SDK. Know which side of the line the scenario sits on.
A few mechanics complete the picture. The Python v2 decorators generate the same metadata the older function.json files declared — portal views still show that JSON shape, so recognize both representations. Blueprints group related functions into modules that register onto one app, keeping large apps organized. Triggers support declarative retry policies (fixed delay or exponential backoff) for transient handler failures, layered beneath the transport's own redelivery. Queue triggers process messages in configurable batches, and per-instance concurrency settings govern how many executions share a worker — the knobs behind "our function processes too many messages at once" scenarios. Finally, the Event Grid trigger differs from a plain HTTP webhook precisely in the handshake: the trigger handles subscription validation automatically, while a raw HTTP endpoint must implement the echo itself — a reliable exam discriminator between the two designs.
Local development closes the loop: Azure Functions Core Tools (func start) runs the same runtime on your machine, with the Azurite emulator standing in for the storage account the runtime requires. Triggers fire locally against real or emulated services, so binding declarations get exercised before any deployment — the fastest way to catch a misnamed connection setting or a wrong binding expression. Settings for local runs live in local.settings.json, which mirrors app settings without ever being deployed; keeping secrets out of it (and out of source control) follows the same discipline as production. When a scenario contrasts 'worked locally, fails deployed,' the diff is almost always an app setting present in one environment and missing in the other — the local file and the cloud configuration drifted.
Reflection Question: What does the runtime do for you between "message arrives on embed-jobs" and "your first line of code executes" — and what does that imply about where connection errors surface?