Google Cloud Functions skill for Codex
Event handlers that survive at-least-once delivery. Codex writes HTTP and CloudEvent handlers with the Functions Framework, runs them locally with a curl'd CloudEvent, and deploys with separate runtime and trigger service accounts. Event handlers deduplicate on a domain key and split transient from permanent errors, so turning on retries never creates an endless loop or a double charge.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- gcp-cloud-functions Installs to ~/.agents/skills/gcp-cloud-functions/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install gcp-cloud-functions The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project gcp-cloud-functions Installs to .agents/skills/gcp-cloud-functions/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/gcp-cloud-functions.zipunzip -o gcp-cloud-functions.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) gcp-cloud-functions PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $gcp-cloud-functions, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Builds and deploys Google Cloud Functions 2nd gen (now Cloud Run functions) with the Functions Framework for Node.js, Python and Go - HTTP handlers, CloudEvent handlers for Pub/Sub, Cloud Storage and Eventarc triggers, retries, idempotency, secrets, concurrency and service accounts. Use when writing or deploying a GCP function, wiring a bucket or Pub/Sub trigger, running a function locally, migrating 1st gen functions, or fixing trigger permission and retry-loop problems.
Use it when
- Writing or deploying a GCP function from source
- Wiring Pub/Sub, Cloud Storage, Firestore or audit-log triggers
- A trigger deploys but never fires, or fires in a loop
- Moving 1st gen functions to 2nd gen
Use something else when
- You ship a container or a multi-route API: use gcp-cloud-run
- The project is on Firebase and uses Firebase SDK triggers: use firebase-firestore
How to check the result
The function reports ACTIVE, a real test event yields one success log and the expected output, and replaying the same event causes no second side effect.
The mistake it prevents
Turning on retries for a handler that throws on malformed input, which redelivers the same poison event for a day.
Done well means: the handler runs locally with the Functions Framework before it is deployed, event handlers are idempotent and decide explicitly which errors retry, the function and its trigger each use a dedicated service account, and secrets come from Secret Manager.
2nd gen functions are Cloud Run services built from source by Cloud Build; Google renamed
the product “Cloud Run functions” in 2024. gcloud functions deploy --gen2 still works and
is used below. Everything in gcp-cloud-run about scaling and IAM applies underneath.
Before writing code
- Read
AGENTS.md, the existingpackage.json/requirements.txt/go.mod, and any deploy script or Terraform. Match the runtime version already in use. - Check what is deployed (needs network and gcloud auth):
gcloud functions list --regions="$REGION"
gcloud functions describe thumbnail --region="$REGION" --format=yaml
gcloud functions runtimes list --region="$REGION" # valid --runtime values
Pick the trigger
| Source | Deploy flags | Handler type |
|---|---|---|
| HTTPS call, webhook | --trigger-http | HTTP |
| Pub/Sub topic | --trigger-topic=orders | CloudEvent |
| Object finalized in a bucket | --trigger-event-filters=type=google.cloud.storage.object.v1.finalized --trigger-event-filters=bucket=uploads | CloudEvent |
| Firestore document write | --trigger-event-filters=type=google.cloud.firestore.document.v1.written --trigger-event-filters=database=(default) --trigger-event-filters-path-pattern=document='users/{id}' | CloudEvent |
| Audit-logged API call | --trigger-event-filters=type=google.cloud.audit.log.v1.written plus serviceName and methodName filters | CloudEvent |
| Schedule | HTTP function + Cloud Scheduler job with an OIDC token | HTTP |
If the work runs longer than about 9 minutes, needs a custom container, or bundles several routes, deploy a Cloud Run service instead.
Handlers
Node.js (npm i @google-cloud/functions-framework):
import functions from "@google-cloud/functions-framework";
functions.http("api", async (req, res) => {
if (req.method !== "POST") return res.status(405).send("method not allowed");
res.json({ ok: true });
});
functions.cloudEvent("onUpload", async (event) => {
const { bucket, name, generation } = event.data; // storage object payload
const key = `${bucket}/${name}#${generation}`; // idempotency key
if (await alreadyProcessed(key)) return; // duplicate delivery: ack
await makeThumbnail(bucket, name);
await markProcessed(key);
});
Python (functions-framework in requirements.txt):
import base64, json
import functions_framework
@functions_framework.cloud_event
def on_order(cloud_event):
msg = cloud_event.data["message"]
order = json.loads(base64.b64decode(msg["data"]))
if not order.get("id"):
print(json.dumps({"severity": "WARNING", "message": "dropping order without id"}))
return # permanent error: return normally so it is NOT retried
process(order) # raise on transient errors to trigger a retry
Initialise clients (DB pools, SDK clients) at module scope so warm instances reuse them; never keep per-request state there.
Run locally
npx @google-cloud/functions-framework --target=api --port=8080
functions-framework --target=on_order --signature-type=cloudevent --debug # Python
curl -X POST localhost:8080 -H 'Content-Type: application/json' \
-H 'ce-id: 1' -H 'ce-specversion: 1.0' -H 'ce-source: //pubsub' \
-H 'ce-type: google.cloud.pubsub.topic.v1.messagePublished' \
-d '{"message":{"data":"eyJpZCI6IjQyIn0="}}'
Unit-test the business function separately from the framework wrapper; it is faster and needs no emulator.
Deploy
gcloud functions deploy on-upload --gen2 --region="$REGION" \
--runtime=nodejs22 --source=. --entry-point=onUpload \
--trigger-event-filters=type=google.cloud.storage.object.v1.finalized \
--trigger-event-filters=bucket=uploads-prod \
--trigger-location=europe-west1 \
--service-account=thumbs-runtime@"$PROJECT".iam.gserviceaccount.com \
--trigger-service-account=thumbs-trigger@"$PROJECT".iam.gserviceaccount.com \
--set-secrets=API_KEY=vision-key:2 \
--memory=512Mi --timeout=300s --max-instances=20 --retry
gcloud functions deploy api --gen2 --region="$REGION" --runtime=python312 \
--source=. --entry-point=api --trigger-http --no-allow-unauthenticated \
--concurrency=20 --cpu=1 --memory=512Mi
- Use a runtime from
gcloud functions runtimes list; do not guess the newest one. --concurrencyabove 1 requires--cpuof at least 1 and a thread-safe handler.- Event-driven timeouts cap at 540 seconds; HTTP functions allow up to 3600 seconds.
.gcloudignorecontrols what is uploaded; excludenode_modules,.venv, tests,.env.- Deploying to production is outward-facing: show the command and target project, and wait for user confirmation.
Permissions that make triggers work
| Identity | Needs |
|---|---|
Runtime SA (--service-account) | Only what the code touches, e.g. roles/storage.objectUser on the bucket |
Trigger SA (--trigger-service-account) | roles/run.invoker on the function, roles/eventarc.eventReceiver on the project |
| Cloud Storage service agent | roles/pubsub.publisher on the project, for Storage triggers |
| Pub/Sub service agent | roles/iam.serviceAccountTokenCreator on the trigger SA (older projects) |
| Build SA | Cloud Build may run as the default compute SA; pass --build-service-account to use a dedicated one with roles/cloudbuild.builds.builder |
Most “trigger deployed but nothing happens” reports are a missing role in this table, or a trigger location that does not match the bucket’s location.
Retries and idempotency
- Without
--retry, a failed event is dropped. With--retry, it is retried with backoff until the event age limit (about 24 hours for Pub/Sub-backed triggers). A handler that always throws on a bad message becomes a retry loop and a cost spike. - Classify errors: transient (timeouts, 429, 5xx) raise; permanent (validation, missing object) log and return.
- Delivery is at-least-once. Deduplicate on
event.idor a domain key (object generation, order ID) stored in Firestore or the DB with a create-if-absent write. - For poison messages, configure a dead-letter topic on the underlying Pub/Sub subscription.
- A Storage-triggered function that writes back into the same bucket retriggers itself. Write outputs to another bucket or prefix and filter on it in code.
Migrating 1st gen
| 1st gen | 2nd gen |
|---|---|
exports.fn = (req, res) / (data, context) | functions.http / functions.cloudEvent with a CloudEvent |
| One request per instance | --concurrency up to 1000 |
--trigger-resource / --trigger-event | --trigger-event-filters (Eventarc) |
| Max 540 s for all | 3600 s for HTTP |
Deploy the 2nd gen function under a new name, shift callers or triggers, then delete the 1st gen function only after the user confirms.
Gotchas
--allow-unauthenticatedon an HTTP function makes it public; ask before using it.- Background work after
res.send()or after the CloudEvent handler resolves is throttled and may never run. Await everything. - Large
node_modulesuploads slow builds; let Cloud Build install dependencies from the lockfile instead. - Environment variable changes create a new revision;
--update-env-varsmerges,--set-env-varsreplaces all. - Logs from
console.logof a JSON object string withseveritybecome structured entries.
Verification
gcloud functions describe on-upload --region="$REGION" --format='value(state,serviceConfig.uri,eventTrigger.retryPolicy)'
gcloud functions logs read on-upload --region="$REGION" --limit=50
gcloud storage cp ./sample.jpg gs://uploads-prod/test/sample.jpg # fire a real event (confirm first in prod)
- State is
ACTIVE; the test event produces exactly one success log and the expected output. - Sending the same event twice produces one side effect, not two.
Report template
**Function:** on-upload (gen2, nodejs22, europe-west1), entry point onUpload
**Trigger:** storage object finalized on uploads-prod, retry on, trigger SA thumbs-trigger@
**Runtime SA:** thumbs-runtime@ with storage.objectUser on uploads-prod and thumbs-prod
**Idempotency:** key bucket/name#generation in Firestore collection processed_uploads
**Tested:** local CloudEvent curl, one staging upload, duplicate event ignored What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Google Cloud Functions"
short_description: "Event handlers that survive at-least-once delivery"
default_prompt: "$gcp-cloud-functions Write a Cloud Function that makes a thumbnail whenever an image lands in this bucket" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.