Tag: Google Cloud Run

  • Notion Isn’t the Everything App. It’s the Everything Database — and That’s a Better Bet.

    Notion Isn’t the Everything App. It’s the Everything Database — and That’s a Better Bet.

    Last refreshed: May 15, 2026

    Update — May 15, 2026: On May 13, 2026, Notion shipped the Notion Developer Platform (version 3.5), with Claude as a launch partner. The platform adds Workers, database sync, an External Agents API, and a Notion CLI. For the full breakdown of what changed and what it means for the Notion + Claude stack, see Notion Developer Platform Launch (May 13, 2026). For the underlying operating philosophy, see The Three-Legged Stack: Notion + Claude + Google Cloud.

    Everyone is building the everything app. Microsoft wants to be yours. Google wants to be yours. Notion wants to be yours. But there’s a fourth path nobody is talking about — and it might be the smartest play for brands, agencies, and multi-system operators: don’t pick one everything app. Build one everything database, and let it feed all of them.

    The Core Idea Notion isn’t competing to be your everything app. It’s competing to be your everything database — the structured, queryable, agent-ready source of truth that sits underneath whatever surface you use. The everything app becomes interchangeable. The database is the moat.

    The Series So Far — and Why This Frame Changes Everything

    This is the fourth piece in a series examining who wins the everything-app race. We looked at Microsoft stitching together an everything app through acquisitions, Google trying to unify a native stack it keeps fragmenting, and Notion building from the database up. Each piece treated the everything app as the destination.

    But there’s a reframe worth making. What if the everything app isn’t the destination? What if the destination is the data layer underneath it — and the everything app is just whichever surface happens to be most useful at a given moment?

    That’s the angle that emerged from actually building inside Notion Workers alpha. And it changes the strategic calculus significantly for anyone running a brand, an agency, or a multi-system operation.

    Your Brand Doesn’t Need One Everything App. It Needs One Everything Database.

    Think about what an everything app actually requires to work. It needs to know your tasks. Your projects. Your contacts. Your content calendar. Your pipeline. Your team’s status. Your historical decisions. Your brand voice. Your client relationships. Your automation outputs.

    That’s not an app problem. That’s a data structure problem. And the company that solves the data structure problem — that gives you a clean, typed, queryable, agent-ready home for all of that — wins, regardless of which surface you use to view it.

    Notion’s database architecture is purpose-built for exactly this. Every property is typed. Every row is queryable. Every database can be filtered, sorted, related, and rolled up. When you build your brand’s operational data inside Notion — tasks with statuses, projects with owners, content with metadata, contacts with relationship history — you’re not just organizing. You’re building a structured intelligence layer that agents can read, write, and reason over reliably.

    That database doesn’t care which “everything app” sits on top of it. Microsoft Copilot can query it. Google Workspace agents can sync from it. Your own custom dashboard can read it via the Public API. Claude can operate on it directly. The surface is interchangeable. The database is the thing that compounds in value over time.

    The 30-Second Trigger: Where the Architecture Gets Interesting

    Here’s the piece that came out of our own Workers alpha experience — and it reframes the “30-second sandbox limitation” from a constraint into a feature.

    Notion Workers runs in a 30-second execution window. We hit that wall hard when we tried to move heavy automations — multi-site WordPress optimization passes, content pipelines, image generation — into Workers. Those are multi-minute jobs. They don’t fit.

    But 30 seconds is more than enough to do one specific thing: fire a signed HTTP POST to an external endpoint and return.

    That’s the architectural insight. You don’t use Notion Workers to execute heavy work. You use Notion Workers to trigger it. The Worker wakes up — on a schedule, on a database change, on a webhook — reads the relevant Notion database row, constructs a signed payload, fires a POST to a Google Cloud Run job, and exits. The whole thing takes under five seconds. Well within the 30-second window.

    Cloud Run picks up the job, runs for as long as it needs — minutes, not seconds — and when it’s done, it writes the results back to the Notion database via the Public API. The Notion database is now the job queue, the status tracker, the results store, and the orchestration log. All in one place. All queryable by agents.

    The pattern in practice:

    Notion Worker (cron / DB change / webhook)
      → reads Notion database row for job config
      → signs POST to Cloud Run endpoint
      → returns immediately (3–8 seconds, well under 30s)

    Cloud Run (no time limit)
      → runs heavy job (WP optimization, pipeline, image gen)
      → writes status + results back to Notion DB via Public API

    Notion Database
      → job queue / status tracker / results store / audit log
      → queryable by agents, visible to team, triggerable again

    This is the hybrid architecture we’re running. Our Tuesday 18-site WordPress SEO optimization pass runs on Cloud Run — not because Notion can’t orchestrate it, but because Notion does orchestrate it, as the database layer, while Cloud Run handles the execution. The Worker is the tickle. Cloud Run is the muscle. Notion is the brain that remembers everything.

    What “Brand Everything Database” Actually Means in Practice

    If you’re an agency, a media operation, or a multi-brand operator, here’s the concrete version of this architecture:

    • One Notion workspace as the brand OS. Every client, project, task, content piece, automation job, and decision lives as structured database rows. Not documents. Not folders. Typed, relational data.
    • Agents inside Notion prep the data. Custom agents compile status updates, flag stale work, surface blockers, build briefings — all operating on the Notion database directly. The “everything” data is always clean and current because agents are maintaining it continuously.
    • Workers trigger external execution. When a job needs more than 30 seconds — content pipelines, SEO runs, bulk operations — a Worker fires the trigger. Cloud Run executes. Results come back into Notion. The database stays the source of truth.
    • Any surface can consume it. A Copilot user can query the project database through Microsoft Graph connectors. A Google Workspace user can sync from Notion via the connector ecosystem. A custom dashboard can read the Notion API. The front end doesn’t matter. The database is always current.
    • External agents get full context. Through the External Agents API, Claude, Codex, or any agent you build can operate against your Notion databases with complete organizational context — not a generic AI, but one that knows your specific data, your specific projects, your specific brand.

    Why This Beats Betting on One Everything App

    The everything-app race has a winner-take-all framing that may be wrong. Here’s what we’ve observed from operating across Microsoft, Google, and Notion simultaneously:

    Different team members live in different surfaces. Your developer lives in GitHub and a terminal. Your account manager lives in Gmail. Your ops lead lives in a spreadsheet. Your creative lead lives in Figma. Forcing everyone onto one everything app means fighting human behavior, not working with it.

    But if everyone’s work — regardless of where they do it — writes back into a shared Notion database? The everything app problem disappears. You don’t need everyone in the same surface. You need everyone’s data in the same structure.

    That’s what Notion’s connector ecosystem is actually building toward. GitHub syncs into Notion. Jira syncs into Notion. Salesforce syncs into Notion. Slack syncs into Notion. The surface stays wherever it is. The intelligence layer centralizes.

    The Compounding Advantage

    Here’s the strategic reason this matters beyond the technical architecture: databases compound. Documents don’t.

    A Google Doc from two years ago is mostly dead weight — hard to search, hard to query, impossible for an agent to reason over reliably. A Notion database from two years ago is a living asset. Every row is still queryable. Every relationship still works. The history of every project, every decision, every outcome is structured data that an agent can analyze, pattern-match against, and use to inform current work.

    The longer you run your brand’s operations through a Notion database, the smarter your agents get — because they have more structured history to work with. That’s not true of any document-first system. And it’s not something you can easily replicate once a competitor has two years of structured operational data and you’re starting from scratch.

    The everything app you pick in 2026 matters less than the data structure you commit to in 2026. Pick the wrong everything app and you switch in 18 months. Pick the wrong data structure and you’re rebuilding from zero.

    The Practical Starting Point

    If this architecture makes sense for your operation, here’s how to think about the starting point:

    • Audit what data your business actually runs on. Tasks, projects, clients, content, pipelines, automations — map out what you’re currently tracking and where. How much of it is in documents? How much is in structured databases?
    • Pick the three databases that matter most and build them right in Notion. Don’t try to migrate everything at once. Start with your project tracker, your content calendar, and your client/contact database. Get those typed, relational, and agent-ready.
    • Connect one external source via Workers or the connector ecosystem. Slack, GitHub, Jira — pick the one that generates the most signal for your operation and get it syncing into Notion.
    • Build one Custom Agent that works on those databases. A status compiler, a blocker detector, a briefing builder — something that demonstrates the database-first advantage concretely to your team.
    • Then consider the trigger pattern. What jobs in your operation take longer than 30 seconds but could be triggered from a database change? Those are your first Cloud Run candidates, with Notion as the orchestration layer.

    The everything app race is real. But the more durable competitive advantage is the data structure underneath it. Build the database right, and the everything app becomes a detail.

    Frequently Asked Questions

    What is a “brand everything database” in Notion?

    A brand everything database is a Notion workspace architected as the structured, queryable source of truth for all of a brand’s operational data — tasks, projects, content, clients, automations, decisions. Unlike document-based systems, every piece of information is typed, relational, and agent-readable. External tools sync into it; agents operate on it; any surface can consume it via the Public API.

    How do Notion Workers act as triggers for Google Cloud Run?

    Notion Workers run in a 30-second sandbox — enough time to read a Notion database row, construct a signed payload, and fire an HTTP POST to a Cloud Run endpoint. The Worker returns immediately; Cloud Run handles the long-running execution (minutes, not seconds) and writes results back to the Notion database via the Public API. This makes Notion the orchestration and visibility layer without hitting the sandbox time limit.

    Why is a database-first architecture better than document-first for AI agents?

    Documents require AI to infer structure from prose — an error-prone process that degrades at scale. Database rows are typed, structured, and directly queryable. An agent asking “which projects are blocked this week?” gets an exact filter result from a Notion database in milliseconds; the same question against a folder of Google Docs produces a best-effort summary. Reliability and precision are the key differences.

    Can Notion databases feed Microsoft Copilot or Google Workspace agents?

    Yes, via connectors and the Notion Public API. Microsoft Graph connectors and Google Workspace connectors can sync from Notion databases. Custom agents built on the External Agents API can also read and write Notion data from any external platform. The Notion database becomes the shared source of truth regardless of which AI surface your team prefers.

    What’s the best first step to building a brand everything database in Notion?

    Start with three core databases: a project tracker, a content calendar, and a client/contact database. Get them typed with proper properties, linked relationally, and cleaned up. Then build one Custom Agent that operates on those databases — a status compiler or briefing builder. Once you’ve seen the database-first advantage in action, the architecture for connecting external tools and Cloud Run triggers becomes obvious.

  • GCP-Powered CRM Touch Calendar Automation for Restoration Companies: Architecture Guide

    GCP-Powered CRM Touch Calendar Automation for Restoration Companies: Architecture Guide

    Who this is for: Your IT person, your developer, or a technical contractor. This brief describes a production-grade automation architecture for the restoration company CRM touch calendar using Google Cloud Platform (GCP). It assumes basic familiarity with cloud infrastructure, command line tools, and web APIs. It does not require deep expertise in any single area — the implementation is intentionally modular so that each component can be handed to a different person if needed.

    The business strategy this automates is in Your CRM Is Not a Lead Database. The manual version of this system is in the Email Automation Setup Guide. This brief is for teams who want to reduce ongoing manual work and build a more robust, scalable version of the same workflow.


    What This Architecture Automates

    The manual system requires a person to: export contacts from the CRM, validate emails, import to Mailchimp or Brevo, configure each campaign, schedule it, and log results back to Notion. For 4–6 campaigns per year, this is manageable manually. For a company running 10–15 campaigns across multiple divisions or service areas, or for an agency running this system for multiple restoration clients, a GCP automation layer eliminates the recurring labor.

    What this architecture handles automatically:

    • Scheduled contact export from ServiceTitan or Jobber via API
    • Segmentation and deduplication logic
    • Email validation pass before import
    • Contact import to Mailchimp or Brevo
    • Campaign creation from template stored in Cloud Storage
    • Campaign scheduling per the calendar in Notion
    • Results logging back to Notion after send

    What still requires human review:

    • Email copy review before scheduling (always — no automation should skip this)
    • Reply triage and qualitative logging
    • Warmth scoring and super-connector identification

    Prerequisites

    • A Google Cloud Platform account with billing enabled (new GCP accounts include $300 in free credits)
    • ServiceTitan or Jobber API access (ServiceTitan requires contacting their enterprise team; Jobber API is available on Connect plan and above at $119–$169/month)
    • Mailchimp account with API access (available on all paid plans) OR Brevo with API access (all plans)
    • Notion account with Notion API integration enabled (free at notion.com/my-integrations)
    • Basic Python familiarity (this implementation uses Python 3.11+)

    Architecture Overview

    ┌─────────────────────────────────────────────────────────────┐
    │                    TRIGGER LAYER                            │
    │  Cloud Scheduler → cron job on campaign dates from Notion   │
    └────────────────────────────┬────────────────────────────────┘
                                 │
    ┌────────────────────────────▼────────────────────────────────┐
    │                 ORCHESTRATION LAYER (Cloud Run)             │
    │  campaign-runner service — reads Notion calendar,           │
    │  determines which campaigns are due, triggers pipeline      │
    └────────────────────────────┬────────────────────────────────┘
                                 │
             ┌───────────────────┼───────────────────┐
             │                   │                   │
    ┌────────▼────────┐ ┌───────▼────────┐ ┌────────▼────────┐
    │  CONTACT SYNC   │ │ TEMPLATE STORE │ │  RESULTS LOGGER │
    │  Cloud Run      │ │  Cloud Storage │ │  Cloud Run      │
    │  - CRM export   │ │  - Email copy  │ │  - Poll email   │
    │  - Segment      │ │  - Subject     │ │    platform     │
    │  - Dedupe       │ │    variants    │ │  - Write Notion │
    │  - Validate     │ │  - Prompt lib  │ │  - Update touch │
    │  - Import to    │ │                │ │    log          │
    │    email        │ └────────────────┘ └─────────────────┘
    │    platform     │
    └─────────────────┘
    

    Component 1: GCP Project Setup

    # Install gcloud CLI and authenticate
    gcloud auth login
    gcloud projects create restoration-crm-[yourcompany] --name="Restoration CRM Automation"
    gcloud config set project restoration-crm-[yourcompany]
    
    # Enable required APIs
    gcloud services enable \
      run.googleapis.com \
      cloudscheduler.googleapis.com \
      secretmanager.googleapis.com \
      storage.googleapis.com
    
    # Create service account for the automation
    gcloud iam service-accounts create crm-automation-sa \
      --display-name="CRM Automation Service Account"
    
    # Grant necessary permissions
    gcloud projects add-iam-policy-binding restoration-crm-[yourcompany] \
      --member="serviceAccount:crm-automation-sa@restoration-crm-[yourcompany].iam.gserviceaccount.com" \
      --role="roles/run.invoker"
    

    Component 2: Secret Manager for API Credentials

    Store all API credentials in GCP Secret Manager. Never hardcode credentials in source code.

    # Store each credential as a separate secret
    echo -n "your-servicetitan-api-key" | gcloud secrets create servicetitan-api-key \
      --data-file=-
    
    echo -n "your-jobber-api-key" | gcloud secrets create jobber-api-key \
      --data-file=-
    
    echo -n "your-mailchimp-api-key" | gcloud secrets create mailchimp-api-key \
      --data-file=-
    
    echo -n "your-notion-token" | gcloud secrets create notion-token \
      --data-file=-
    
    # In Python, access secrets like this:
    # from google.cloud import secretmanager
    # client = secretmanager.SecretManagerServiceClient()
    # name = f"projects/{project_id}/secrets/{secret_id}/versions/latest"
    # response = client.access_secret_version(request={"name": name})
    # secret_value = response.payload.data.decode("UTF-8")
    

    Component 3: Contact Sync Service

    This Cloud Run service handles the contact export → segment → validate → import pipeline. Deploy as a container triggered by the orchestration layer.

    # contact_sync/main.py
    
    import os
    import json
    import requests
    from google.cloud import secretmanager
    
    def get_secret(secret_id):
        client = secretmanager.SecretManagerServiceClient()
        project_id = os.environ.get("GCP_PROJECT_ID")
        name = f"projects/{project_id}/secrets/{secret_id}/versions/latest"
        response = client.access_secret_version(request={"name": name})
        return response.payload.data.decode("UTF-8")
    
    def export_jobber_contacts():
        """Export residential clients from Jobber API"""
        api_key = get_secret("jobber-api-key")
        
        # Jobber uses GraphQL API
        query = """
        query GetClients($after: String) {
          clients(first: 100, after: $after) {
            nodes {
              id
              firstName
              lastName
              emails { address isPrimary }
              tags { label }
              jobs(first: 1) {
                nodes { jobType completedAt }
              }
            }
            pageInfo { hasNextPage endCursor }
          }
        }
        """
        
        headers = {
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json"
        }
        
        contacts = []
        cursor = None
        
        while True:
            variables = {"after": cursor} if cursor else {}
            response = requests.post(
                "https://api.getjobber.com/api/graphql",
                headers=headers,
                json={"query": query, "variables": variables}
            )
            data = response.json()
            clients = data["data"]["clients"]
            
            for client in clients["nodes"]:
                email = next(
                    (e["address"] for e in client["emails"] if e["isPrimary"]),
                    client["emails"][0]["address"] if client["emails"] else None
                )
                if not email:
                    continue
                
                # Determine segment based on tags
                tags = [t["label"].lower() for t in client["tags"]]
                if "residential" in tags or not any(t in tags for t in ["commercial", "adjuster", "vendor"]):
                    segment = "Homeowner"
                elif any(t in tags for t in ["adjuster", "agent", "insurance"]):
                    segment = "Industry"
                else:
                    segment = "Trade"
                
                # Get most recent job type
                job_type = None
                if client["jobs"]["nodes"]:
                    job_type = client["jobs"]["nodes"][0].get("jobType", "")
                
                contacts.append({
                    "first_name": client["firstName"],
                    "last_name": client["lastName"],
                    "email": email.lower().strip(),
                    "segment": segment,
                    "job_type": job_type or ""
                })
            
            if not clients["pageInfo"]["hasNextPage"]:
                break
            cursor = clients["pageInfo"]["endCursor"]
        
        return contacts
    
    def deduplicate_contacts(contacts):
        """Remove duplicate emails, keep most recent record"""
        seen = {}
        for contact in contacts:
            email = contact["email"]
            if email not in seen:
                seen[email] = contact
        return list(seen.values())
    
    def segment_contacts(contacts):
        """Split into three segment lists"""
        segments = {"Homeowner": [], "Industry": [], "Trade": []}
        for contact in contacts:
            seg = contact.get("segment", "Homeowner")
            if seg in segments:
                segments[seg].append(contact)
        return segments
    
    def import_to_mailchimp(contacts, tag, api_key, list_id):
        """Batch import contacts to Mailchimp with tag"""
        
        # Mailchimp batch operations (max 500 per call)
        batch_size = 500
        
        for i in range(0, len(contacts), batch_size):
            batch = contacts[i:i+batch_size]
            
            operations = []
            for contact in batch:
                operations.append({
                    "method": "PUT",
                    "path": f"/lists/{list_id}/members/{contact['email'].encode().hex()}",
                    "body": json.dumps({
                        "email_address": contact["email"],
                        "status_if_new": "subscribed",
                        "merge_fields": {
                            "FNAME": contact.get("first_name", ""),
                            "LNAME": contact.get("last_name", ""),
                            "JOB_TYPE": contact.get("job_type", "")
                        },
                        "tags": [tag]
                    })
                })
            
            response = requests.post(
                "https://us1.api.mailchimp.com/3.0/batches",
                auth=("anystring", api_key),
                json={"operations": operations}
            )
            
            if response.status_code not in [200, 201]:
                raise Exception(f"Mailchimp batch import failed: {response.text}")
        
        return len(contacts)
    
    def run_contact_sync(request):
        """Main Cloud Run handler"""
        mailchimp_api_key = get_secret("mailchimp-api-key")
        mailchimp_list_id = os.environ.get("MAILCHIMP_LIST_ID")
        
        contacts = export_jobber_contacts()
        contacts = deduplicate_contacts(contacts)
        segments = segment_contacts(contacts)
        
        results = {}
        for segment_name, segment_contacts in segments.items():
            count = import_to_mailchimp(
                segment_contacts,
                tag=segment_name,
                api_key=mailchimp_api_key,
                list_id=mailchimp_list_id
            )
            results[segment_name] = count
        
        return json.dumps({"status": "success", "imported": results})
    

    Component 4: Cloud Scheduler Trigger

    Cloud Scheduler runs the orchestration service on the campaign dates stored in your Notion calendar. The scheduler checks Notion weekly for upcoming campaigns and pre-triggers the contact sync 7 days before each scheduled send.

    # Create weekly scheduler job
    gcloud scheduler jobs create http crm-weekly-check \
      --schedule="0 9 * * 1" \
      --uri="https://[cloud-run-url]/check-upcoming-campaigns" \
      --oidc-service-account-email="crm-automation-sa@[project].iam.gserviceaccount.com" \
      --time-zone="America/Los_Angeles" \
      --location="us-west1"
    

    The orchestration service reads your Notion Campaign Calendar database, finds any campaigns with a send date within the next 7 days and a Status of “Scheduled,” and triggers the contact sync and campaign creation pipeline for each one.


    Component 5: Results Logger

    After each campaign sends, this service polls the Mailchimp or Brevo API for campaign analytics and writes them back to your Notion Campaign Calendar database.

    # results_logger/main.py (simplified)
    
    def log_campaign_results(campaign_id, notion_page_id):
        mailchimp_api_key = get_secret("mailchimp-api-key")
        notion_token = get_secret("notion-token")
        
        # Get Mailchimp campaign report
        response = requests.get(
            f"https://us1.api.mailchimp.com/3.0/reports/{campaign_id}",
            auth=("anystring", mailchimp_api_key)
        )
        report = response.json()
        
        open_rate = report.get("opens", {}).get("open_rate", 0)
        
        # Update Notion page
        notion_headers = {
            "Authorization": f"Bearer {notion_token}",
            "Content-Type": "application/json",
            "Notion-Version": "2022-06-28"
        }
        
        requests.patch(
            f"https://api.notion.com/v1/pages/{notion_page_id}",
            headers=notion_headers,
            json={
                "properties": {
                    "Status": {"select": {"name": "Sent"}},
                    "Open Rate": {"number": round(open_rate * 100, 1)}
                }
            }
        )
    

    Estimated Monthly GCP Costs

    For a single restoration company running 6 campaigns per year:

    Service Usage Monthly Cost
    Cloud Run (contact sync) 6 invocations/year, <5min each <$1
    Cloud Scheduler 52 weekly checks/year $0.10
    Cloud Storage (templates) Minimal storage <$0.01
    Secret Manager 4 secrets, <1000 accesses/month <$0.10
    Total <$2/month

    For an agency running this system for 10 restoration clients simultaneously, the cost scales linearly — approximately $15–20/month in GCP costs for the full multi-client operation. The manual labor savings at that scale are significant: an estimated 8–12 hours per month of manual campaign setup eliminated.


    Deployment Checklist

    • GCP project created and APIs enabled
    • Service account created with appropriate permissions
    • All API credentials stored in Secret Manager
    • Contact sync service containerized and deployed to Cloud Run
    • Cloud Scheduler job created and tested
    • Notion Campaign Calendar database connected
    • Results logger deployed and tested with a historical campaign
    • Full end-to-end test run on a staging contact list before live deployment

    Full documentation for each GCP service referenced here: cloud.google.com/run/docs, cloud.google.com/scheduler/docs, cloud.google.com/secret-manager/docs.


  • Wp Proxy Pattern Cloud Run — Article Hero Images Visual

    Wp Proxy Pattern Cloud Run — Article Hero Images Visual

    The WP Proxy Pattern: How We Route 19 WordPress Sites Through One Cloud Run Endpoint
    The WP Proxy Pattern: How We Route 19 WordPress Sites Through One Cloud Run Endpoint

    About This Image

    This image is part of the Article Hero Images collection in the Tygart Media visual library. Every image produced by Tygart Media is AI-generated using Google Vertex AI (Imagen), converted to WebP format, and injected with full IPTC/XMP metadata before publication.

    Technical Details

    • Format: WEBP
    • Collection: Article Hero Images
    • Media ID: 357
    • Pipeline: Vertex AI Imagen → WebP → IPTC/XMP → WordPress

    Image Licensing

    All images in the Tygart Media visual library are produced in-house using AI image generation and are owned by Tygart Media.