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.

  • Cloud Platforms
  • $gcp-cloud-functions
  • 192 lines
  • 2 files
  • ~2,150 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- gcp-cloud-functions

Installs to ~/.agents/skills/gcp-cloud-functions/ for every project on this machine.

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

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.

~/.agents/skills/gcp-cloud-functions/SKILL.md

--- name: gcp-cloud-functions description: Builds and deploys Google Cloud Functions 2nd gen (now Cloud Run functions) with the Functions Framework for Node.js, Python and Go - HTTP handlers,… license: MIT ---

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

  1. Read AGENTS.md, the existing package.json / requirements.txt / go.mod, and any deploy script or Terraform. Match the runtime version already in use.
  2. 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

SourceDeploy flagsHandler type
HTTPS call, webhook--trigger-httpHTTP
Pub/Sub topic--trigger-topic=ordersCloudEvent
Object finalized in a bucket--trigger-event-filters=type=google.cloud.storage.object.v1.finalized --trigger-event-filters=bucket=uploadsCloudEvent
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 filtersCloudEvent
ScheduleHTTP function + Cloud Scheduler job with an OIDC tokenHTTP

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.
  • --concurrency above 1 requires --cpu of at least 1 and a thread-safe handler.
  • Event-driven timeouts cap at 540 seconds; HTTP functions allow up to 3600 seconds.
  • .gcloudignore controls what is uploaded; exclude node_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

IdentityNeeds
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 agentroles/pubsub.publisher on the project, for Storage triggers
Pub/Sub service agentroles/iam.serviceAccountTokenCreator on the trigger SA (older projects)
Build SACloud 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.id or 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 gen2nd 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 all3600 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-unauthenticated on 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_modules uploads slow builds; let Cloud Build install dependencies from the lockfile instead.
  • Environment variable changes create a new revision; --update-env-vars merges, --set-env-vars replaces all.
  • Logs from console.log of a JSON object string with severity become 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.yamlgenerated from this page

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.

esc
New

Introducing Codex Skills Pro

79 professional skills that take Codex through complete engagements, each with its own scripts, references and a report that verifies the result.

All-AccessOne-time payment, no subscription
$49
Get All-AccessOr browse individual skills from $2.99 →

Secure checkout by Creem