connect_bigquery
ChatGPTConnects the user's BigQuery to Porter as an export destination. Two paths depending on what the user prefers: `auth_mode="oauth"` (default) — returns a URL. Hand it to the user as a clickable link and STOP; the Porter UI runs the OAuth flow (Google consent + callback + destination creation) end to end. When the user reports back, call `list_bigquery_destinations()` to verify the new destination is there, then continue with `list_bigquery_projects(target_user_id)`. auth_mode="service_account" — the user pastes their GCP service account JSON key into the chat and you pass it through here as a string. Useful when OAuth is blocked by org policies. The JSON must contain type="service_account", project_id, private_key, client_email; format is validated locally before reaching Porter. Treat the JSON key like a secret: do not echo it back, do not store it. After this call resolves with status="ACTIVE", the destination is ready and list_bigquery_destinations() will list it. Either way, the user-facing summary is the same: "BigQuery is connected to Porter. Pick a project to continue." Do not narrate OAuth scopes, target user ids, or callbacks.
connect_new_account
ChatGPTReturns the URL of Porter's authorizations dashboard (no specific component). The user opens it in a browser, picks any third-party (Facebook Ads, Google Ads, BigQuery, …) and connects or manages authorizations. New SourceUsers / TargetUsers become visible immediately in list_authorized_users() and list_bigquery_destinations(). When the agent already knows which component to target, prefer start_user_authorization(component_name) (or connect_bigquery for the BigQuery destination — it also supports the service-account path). The URL carries ?agent=<slug> (resolved from the MCP client) so the page's return target says "Back to <Agent>" instead of generic Porter setup steps.
create_blend
ChatGPTCreates a saved blend — a cross-connector container that scopes the user's selected accounts under a single name. A blend by itself does not run anything; think of it as the container the user names ("Glamping + GXMOTOR Spend"). The actual recurring export to BigQuery is set up by create_blend_schedule_to_bigquery next, which receives the metrics, dimensions, date range and BQ destination. If the user expects a scheduled export, you MUST follow up with that call — leaving a blend without it produces a stub that never runs (this is the classic "I created a blend but it isn't running" pitfall). After persisting the blend, this tool also runs the GetSchema workflow against it so it becomes immediately queryable. Read the response's warmup fields to decide what to do next: - schema_warmed=true ⇒ the blend is queryable now; nothing else needed. - schema_warmed=false + schema_warmup_reauth_url set ⇒ a 3P token expired; surface the URL to the user, then once they confirm they re-authorized call refresh_blend_schema(blend_id) and the blend becomes queryable. - schema_warmed=false + no reauth URL ⇒ generic warmup failure; surface schema_warmup_error and call refresh_blend_schema(blend_id) to retry. The blend record stays intact either way. For ad-hoc data without persistence, prefer query_data (in either mode) — no need to create a blend at all.
create_blend_schedule_to_bigquery
ChatGPTSets up a blend for recurring export to BigQuery and runs it for the first time, in a single call. What the user sees: their blend now writes to a BigQuery table on a schedule, and the destination already has data when the call returns. What happens internally (do NOT narrate to the user): the MCP creates the recurring schedule, persists the saved query, and asks the worker to run once with executionType="MANUAL" so the user does not have to wait for the next cron tick.
create_report
ChatGPTCreate a new shareable Porter report. A report = a frontend bundle (HTML/CSS/JS that you write) + a structured config describing every chart's data needs. The report is packaged into a Cloudflare Worker and returned as a URL the user can open and share. Security boundary: your frontend code never talks to Porter directly. It calls a small SDK (loaded as /sdk.js inside the report) that fetches data only for chart IDs declared in config.charts. Anything else is rejected with 404. ─── REPORT CONFIG SHAPE ─────────────────────────────────────────────────── { "name": "Daily Clicks", "controls": { "date_range": { "default": "last_30_days" } | null, // The keys below are presentation hints — when set, declare them on // every chart that should react to them via applies_controls. "filters": [ { "field": "campaign_name", "label": "Campaign" } ] | null, "sort": true | null, "limit": { "default": 50 } | null }, "charts": { "<chart_id>": { "data_source": "facebook-ads", // connector slug (HYPHENATED, e.g. "facebook-ads", "google-ads") "accounts": [ // FULL DataSourceAccount dicts — the same shape query_data accepts. { // Get them from list_known_accounts(component_name=...) BEFORE "id": "act_123", // creating the report. Bare ID strings (account_ids: ["act_123"]) "name": "Acme Co", // are accepted for backward compat but the chart will fail to load "component_name": "facebook-ads", // because the connector worker can't resolve the SourceUser → it "source_user_id": "<su_id>", // tries to register a "new" account and the subscription quota "company_id": "<co_id>" // check rejects it. } ], "metrics": ["clicks"], // required, non-empty "dimensions": ["date"], // optional "filters": [...], // OPTIONAL baked-in filters: ALWAYS applied // Which dynamic controls re-query this chart at viewer time: // "date_range" → date picker; "filters" → viewer filter pickers; // "sort" → sort dropdown; "limit" → row-cap dropdown. "applies_controls": ["date_range", "filters", "sort", "limit"] } } } Baked-in chart filters always apply (the deploy-api merges them with any viewer-supplied filters). Dynamic controls (date_range, filters, sort, limit) only take effect on charts that list them in applies_controls. BEFORE calling create_report, run list_known_accounts() (or list_authorized_users() + list_user_accounts()) for each connector and embed the FULL DataSourceAccount objects you get back into chart.accounts. Don't strip them down to just the id — the per-chart query needs every field (name, component_name, source_user_id, company_id) to resolve against Porter's connector workers, exactly the same way query_data works. ─── PORTER SDK (window.Porter, exposed inside every deployed report) ────── Load with: <script src="/sdk.js"></script> Porter.fetchChart(chartId, params?) → Promise<{ columns: string[], // ordered list of column NAMES rows: Array<{[columnName: string]: any}>,// each row is an OBJECT keyed by column name meta: {row_count, total_rows, truncated, ...} }> Fetches data for a chart_id declared in your config. Honors the global date range / filters / sort / limit unless you pass them explicitly: params = { startDate?, endDate?, filters?, sort?, limit? }. DATA SHAPE — read carefully, this is where reports break: Each row is an OBJECT (not an array). Read with property access: const spend = row.facebook_ads_spend; // dot const img = row["facebook_ads_image_asset"]; // bracket-by-name OK for (const key of data.columns) { // dynamic columns OK td.textContent = row[key]; } data.columns is the ordered list of column NAMES. Use it to render table headers or iterate row keys in a stable order. It is NOT an array of values — and rows are NOT array-tuples. ❌ NEVER — row[number] on an object returns undefined silently, every metric becomes 0, every dimension becomes "": const iSpend = data.columns.indexOf("spend"); const v = row[iSpend]; ✅ ALWAYS — match the column-name strings declared in config.charts.<id>: const v = row.spend; const v = row[colName]; // colName is a string Column names in rows match EXACTLY what you put in metrics / dimensions of the chart's config. If you wrote metrics: ["facebook_ads_spend"], the key is facebook_ads_spend. If you wrote metrics: ["spend"] (porter-blend unprefixed), the key is spend. There is no transformation in between. IMAGE FIELDS — values like facebook_ads_image_asset come back without a scheme (e.g. media.portermetrics.com/.../X.jpeg). Prepend https:// before using them as <img src>: img.src = row.facebook_ads_image_asset ? "https://" + row.facebook_ads_image_asset : PLACEHOLDER_DATA_URL; Porter.dateRangePicker(elementId, options?) → void Mounts the standard date range picker. Fires porter:daterange. Default presets: Last 7 / 30 / 90 days. Custom dates also supported. Porter.filterPicker(elementId, options) → void Renders a select (or multi-select) bound to one field. Choices update the global filter state and re-trigger the next fetchChart. Options: { field: "campaign_name", // dimension name label?: "Campaign", options: [{ label: "Acme", value: "acme" }, ...], multi?: false, // multi-select → operator "IN" defaultValue?: "acme" | ["acme", "beta"] } Fires porter:filters with the full filter list. Porter.sortControl(elementId, options) → void Single-select sort. options = { options: [{ field, label, direction }], defaultIndex?: 0 }. Fires porter:sort. Porter.limitControl(elementId, options?) → void Single-select row cap. options = { options?: [10, 25, 50, 100], defaultValue?: 25 }. Fires porter:limit. Empty selection clears the cap. Porter.getDateRange() → {startDate, endDate} Porter.getFilters() → Array<{fieldName, operator, values}> Porter.getSort() → Array<{field, direction}> Porter.getLimit() → number | undefined Porter.onDateRangeChange(cb) | onFiltersChange(cb) | onSortChange(cb) | onLimitChange(cb) Subscribe to each control event; pass a re-render function so charts re-fetch when the viewer changes a control. Porter.formatNumber(value, options?) → string Wraps Intl.NumberFormat. Options: { style: "decimal" | "currency" | "percent", currency, notation: "standard" | "compact", maximumFractionDigits, ... } Porter.formatDate(value, format?) → string Format: "iso" (default, YYYY-MM-DD), "long", "short". ─── LOADING / ERROR STATE (auto-managed) ────────────────────────────────── The deployed Worker also serves a default stylesheet at /porter-default.css (auto-injected into the HTML head if you don't link it yourself). It defines: .porter-skeleton — shimmering placeholder; sized by the host. .porter-skeleton-chart — chart-shaped (16:9 ratio, min-height 240px). .porter-skeleton-text — single-line text placeholder. .porter-skeleton-kpi — KPI-number placeholder. .porter-error — red-tinted state for failed chart loads. To get a CONSISTENT loading indicator on initial render AND every refetch (date-range change, filter change, …), put data-porter-chart="<chartId>" on the host element(s). The SDK auto-toggles classes on every Porter.fetchChart(chartId) call: on call: adds .porter-skeleton, removes .porter-error. on resolve: removes .porter-skeleton. on reject: removes .porter-skeleton, adds .porter-error. No manual class management needed in your JS. Multiple elements can be bound to the same chartId — e.g., a chart container AND a sibling KPI grid that share the same fetch. The author's theme can override the palette by setting --porter-skeleton-bg, --porter-skeleton-shimmer, --porter-error-bg, --porter-error-fg, --porter-error-border on :root. Default colors are tuned for dark themes. Use .porter-skeleton-chart directly on the canvas wrapper (gives it a 16:9 placeholder) and remove the inline class once data renders, OR rely entirely on the attribute-driven toggle (the SDK adds/removes .porter-skeleton every call so the loader fires on refetch too). ─── WORKED EXAMPLE ──────────────────────────────────────────────────────── User asks: "FB Ads daily clicks, last 30 days. Date picker AND a Campaign dropdown that filters the chart." Step 1: pull the actual account objects (this would normally come from list_known_accounts(component_name="facebook-ads")): fb_account = { "id": "act_123", "name": "Acme FB Ads", "component_name": "facebook-ads", "source_user_id": "su_abc", "company_id": "co_xyz" } config = { "name": "FB Ads — Daily Clicks", "controls": { "date_range": {"default": "last_30_days"}, "filters": [{"field": "campaign_name", "label": "Campaign"}] }, "charts": { "clicks_by_day": { "data_source": "facebook-ads", "accounts": [fb_account], "metrics": ["clicks"], "dimensions": ["date", "campaign_name"], "applies_controls": ["date_range", "filters"] } } } frontend_html = ''' <link rel="stylesheet" href="style.css"> <header> <h1>FB Ads — Daily Clicks</h1> <div id="picker"></div> <div id="campaign-filter"></div> </header> <!-- data-porter-chart opts this element into the SDK's auto-managed loading lifecycle: .porter-skeleton appears on every fetchChart call (initial load + every refetch from date/filter changes) and is removed when data arrives. .porter-skeleton-chart gives it a default 16:9 shimmer placeholder while data is in flight. --> <div id="chart-wrap" data-porter-chart="clicks_by_day" class="porter-skeleton porter-skeleton-chart"> <canvas id="chart"></canvas> </div> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script src="/sdk.js"></script> <script src="/report.js"></script> ''' frontend_js = ''' Porter.dateRangePicker("picker"); Porter.filterPicker("campaign-filter", { field: "campaign_name", label: "Campaign", options: [ { label: "All campaigns", value: "" }, { label: "Black Friday", value: "bf-2026" }, { label: "Q1 Launch", value: "q1-2026" } ] }); let chart; async function render() { // No need to toggle .porter-skeleton by hand — the SDK does it for any // element with data-porter-chart="clicks_by_day" on every fetchChart call. const data = await Porter.fetchChart("clicks_by_day"); const labels = data.rows.map(r => r.date); const values = data.rows.map(r => r.clicks); if (chart) { chart.data.labels = labels; chart.data.datasets[0].data = values; chart.update(); } else { chart = new Chart(document.getElementById("chart"), { type: "line", data: { labels, datasets: [{ label: "Clicks", data: values }] } }); } } render(); Porter.onDateRangeChange(render); Porter.onFiltersChange(render); '''
delete_report
ChatGPTSoft-delete a report and tear down its Cloudflare Worker. The Report row is marked DELETED (history is preserved); the public URL stops serving immediately.
disable_blend
ChatGPTPauses the recurring run for a blend (the cron stops ticking). The blend configuration stays in place and a one-shot trigger_blend_now still works. Idempotent. Use this when the user says things like "stop the daily refresh for X" or "we don't want this running until next month". Re-activate with enable_blend(blend_id) when ready.
enable_blend
ChatGPTRe-activates the recurring run for a blend (the cron starts ticking again on its configured timezone). Idempotent. The blend's manual trigger (trigger_blend_now) works in both states, so disable_blend is the right move when the user wants to "stop automatic refreshes for now" without losing the configuration. If the blend has no schedule attached, the response says so — that's the agent's cue to ask the user whether to set one up; don't assume.
execute_connector_action
ChatGPTExecute a write action against a third-party connector API. The automations-api validates params, resolves OAuth credentials, and forwards the request. Always call get_action_schema first to discover required params and valid enum values. Actions that create resources (campaigns, ad sets, ads) default to PAUSED status for safety.
get_action_schema
ChatGPTGet the full parameter schema for a connector action. Shows required/optional params, types, enum values, and defaults — everything needed to build a valid execute_connector_action call.
get_bigquery_setup_guide
ChatGPTReturns a markdown step-by-step guide for connecting the user's BigQuery to Porter. Use BEFORE connect_bigquery when the user hasn't prepared credentials yet.
get_blend_schedule
ChatGPTReturns the full record of a schedule (cron expression, timezone, is_enabled, payload). Pair with pause_blend_schedule / resume_blend_schedule to toggle.
get_filter_operators
ChatGPTReturns the catalog of filter operators valid for query_data (both modes). Each entry has a name (matches FieldFilter.operator), a human-readable semantic description, the kind of value the operator expects (scalar/list/none), and a JSON example. Call this only when you need to disambiguate operator semantics (e.g. regex_partial vs contains, in vs equals with a list). For typical query construction, the enum-typed operator field on FieldFilter already surfaces every valid name.
get_looker_setup_guide
ChatGPTReturns a markdown guide for using Porter blends inside Looker Studio via the Porter community connector.
get_powerbi_setup_guide
ChatGPTReturns a markdown guide for using Porter blends inside Power BI Desktop via the Porter custom connector (.mez file).
get_report
ChatGPTGet a report's current config, deployment URL, sharing settings, version history, AND the current source bundle (config, frontend_html, frontend_css, frontend_js). Use this BEFORE calling update_report when a user asks you to edit an existing report ("add a chart", "fix the broken table", "remove that KPI"). Read the current frontend_js + config, modify only what changed, and send the full bundle back via update_report. Do NOT regenerate from scratch — you may reintroduce bugs you already fixed.
list_authorized_users
ChatGPTLists the user's third-party authorizations (SourceUsers) and, for each one, the accounts already known to Porter (SourceAccounts). A user can hold an authorization (e.g. their Facebook user) without ever having used a specific account (page) in a Porter report — in that case the authorization appears here with an empty known_accounts list. To discover the actual accounts the third party reports for that authorization, call list_user_accounts(component_name, source_user_id).
list_bigquery_datasets
ChatGPTLists BigQuery datasets in a given project. Pair with create_blend + create_blend_schedule_to_bigquery to schedule a blend export.
list_bigquery_destinations
ChatGPTLists the BigQuery destinations currently configured for the user's company. Each entry includes target_user_id, project_id, dataset, auth_mode and is_active. Empty list means the user hasn't connected BigQuery yet — call connect_bigquery after walking them through get_bigquery_setup_guide. To set up periodic syncs of a blend into BigQuery, configure the blend with a BigQuery destination from Porter's UI; the schedule is created automatically and visible via list_blends() + get_blend_schedule().
list_bigquery_projects
ChatGPTLists the Google Cloud projects accessible by an authorized BigQuery TargetUser. Use after list_bigquery_destinations() to choose a project, then list_bigquery_datasets(target_user_id, project_id) for the dataset.
list_blend_executions
ChatGPTLists the recorded executions of a blend. Use this for audit questions ("when did this blend last run?") or to surface failures. A Blend may host multiple BlendQueries (one per output — BigQuery, Sheets, …) and history rows are written per-BlendQuery. Pass either: - blend_id — resolves the blend's BlendQueries first, then aggregates their histories newest-first. Best when you only have the Blend uuid (from list_blends() / create_blend()). - blend_query_id — direct, single round-trip. Use when you already have the BlendQuery uuid (e.g., it was just returned by create_blend_schedule_to_bigquery). Takes precedence over blend_id if both are supplied. IMPORTANT — asynchronous gap with trigger_blend_now / create_blend_schedule_to_bigquery: the worker writes the BlendQueryHistory row only AFTER the destination write finishes (a few seconds after the trigger returned first_run_status="successful"). If you call this immediately after a successful trigger and the result is empty, that does NOT mean the run failed — it just hasn't been logged yet. Do not narrate the lag to the user; either wait a few seconds and re-poll, or treat the trigger response as the source of truth and tell the user the data is being written.
list_blends
ChatGPTLists every blend the user has configured. For the user, a blend is "a saved cross-connector report" — that's the abstraction. Each BlendInfo carries schedule_id (set when the blend has a recurring export, e.g. to BigQuery) and destination (slug of the target component). When schedule_id is None the blend exists but no automatic refresh is configured; that's still valid for ad-hoc query_data(blend_id=...) use. Use this as the entry point when the user says "list my blends", "what's running on a schedule?" or wants to manage existing pipelines via enable_blend / disable_blend / trigger_blend_now.
list_connector_actions
ChatGPTList available write actions for a connector (e.g. "facebook-ads"). Returns action summaries — call get_action_schema for full parameter details.
list_custom_fields
ChatGPTReturns user-defined custom fields for one or more connected accounts. Custom fields live OUTSIDE the standard catalog returned by list_fields() — they are configured per-account inside the connector itself (e.g. Google Ads conversion actions, Hubspot custom properties). Call this ONLY when the user references a metric or dimension that did NOT appear in list_fields() for the relevant data source AND list_fields().supports_custom_fields is true (or the account from list_known_accounts() / list_user_accounts() is on a connector that supports them — see the same flag in this response). The response carries one entry per account. An entry with supports_custom_fields=False means the connector does not have custom fields at all — do not retry. An entry with supports_custom_fields=True and an empty fields list means that account has none configured. Field names returned here are valid inputs for query_data exactly like standard fields.
list_data_sources
ChatGPTReturns the catalog of Porter data sources so you can tell the user which sources Porter supports. Use list_authorized_users() to see which connectors the user has already authorized (3P OAuth completed) and list_known_accounts() to see the accounts already used in Porter reports.
list_fields
ChatGPTReturns the available fields for a data source from a local schema catalog. No accounts or auth required. Call this BEFORE query_data to discover valid field names — every name passed to query_data must come from a list_fields response. Call with NO arguments to list porter-blend fields (preferred). Porter-blend is the curated cross-connector schema: fields use unprefixed names (e.g. clicks, ads_impressions, amount_spent) and each one's data_sources attribute lists the connector slugs it covers. When porter-blend's data_sources for a field contains every account.component_name in your query, use the unprefixed blend name instead of the per-connector duplicate (clicks, not facebook_ads_clicks + google_ads_clicks) — Porter merges them into one column at query time. Pass data_source_name="<connector-slug>" only when porter-blend doesn't include the specific metric you need (connector-specific concepts like Facebook's messaging conversions, Shopify's order tags, etc.). Per-connector responses include porter_blend_alternatives — blend field names covering that connector under the SAME field_type/search filters you passed (so the list narrows alongside your query). Prefer those names whenever they fit. Per-connector responses are capped at 500 fields. When truncated=True, the response carries a truncation_hint telling you exactly how to narrow (with field_type, search, or by falling back to porter-blend). Porter-blend itself is never truncated. The response also carries supports_custom_fields. When this is True and the user references a metric that did NOT appear here, call list_custom_fields(accounts=[...]) for the relevant accounts before falling back to a per-connector schema.
list_known_accounts
ChatGPTLists every SourceAccount already created in Porter for the user — i.e. accounts the user has used at least once in a report. Each item is shaped as a DataSourceAccount and can be passed directly to query_data(accounts=...). If empty, the user hasn't run any report yet. Pair this with list_user_accounts to discover accounts the user has access to but hasn't queried before.
list_reports
ChatGPTList the current company's active reports.
list_user_accounts
ChatGPTLists every account the third party reports for an authorized SourceUser (e.g. all Facebook pages the user manages, all Google Ads accounts they can access). Use this when the user wants to query an account they haven't reported on before. Each option carries an id you can plug into query_data(accounts=...) as the DataSourceAccount.id. The is_known_in_system map flags which options already exist as SourceAccounts; the others will be created on-the-fly when first queried.
pause_blend_schedule
ChatGPTPauses a blend-to-destination schedule. Idempotent — pausing an already-paused schedule is a no-op.
preview_report_data
ChatGPTPreview a single chart's data before deploying a report. NOTE: Not yet wired in v1 — call query_data directly during report authoring. Will become a thin chart_config → query_data adapter in a follow-up.
query_data
ChatGPTRun a real-time data query, against either YOUR accounts or a saved blend. Pass exactly one of accounts (live ad-hoc) or blend_id (saved blend) — not both, not neither. The other parameters work the same way in both modes. Mode 1 — accounts (live ad-hoc) Use this whenever you can enumerate the accounts up front: one connector, many accounts of the same connector, or a mix from DIFFERENT connectors. The blend engine merges shared columns when the accounts span connectors (so clicks is one column even across Facebook Ads + Google Ads). REQUIRED for fields: call list_fields(data_source_name=<connector-slug>) for a single-connector query, or list_fields() (no args, porter-blend catalog) when blending across connectors. fields MUST contain only names returned by list_fields() or list_custom_fields(). Mode 2 — blend_id (saved blend) Re-runs a previously-saved blend by id (from list_blends() or create_blend()). The saved blend supplies the account set and schema — you only specify what to project (fields, source from list_fields(), porter-blend unprefixed names), the time window (date_range), and any per-call overrides (filters, limit, sort). Custom fields defined on the saved blend won't carry sort labels (sort labels resolve off the porter-blend catalog only). To materialize a saved blend into BigQuery on a recurring basis, use create_blend_schedule_to_bigquery. Saved-blend failure mode: if this raises BlendSchemaNotWarmedError the blend's schema cache is missing — call refresh_blend_schema(blend_id) once and retry the original query. Do not retry blindly without the refresh; the cause is server-side and requires an explicit warmup. The query always runs against the third party in real time (no Porter-side caching). For recurring exports use the schedule tool instead. Time granularity is controlled by which time-based dimension fields are included (e.g. date for daily, week for weekly). Filters (AND / OR logic) Filters use a nested list structure: list[list[FieldFilter]]. - The outer list combines groups with AND logic. - The inner list combines filters within a group with OR logic. Examples: - Single filter: [[{field, operator, value}]] - AND (country=US AND campaign=X): [[{country=US}], [{campaign=X}]] - OR (country=US OR country=UK): [[{country=US}, {country=UK}]] - Mixed ((campaign=A OR campaign=B) AND country=US): [[{campaign=A}, {campaign=B}], [{country=US}]]
refresh_blend_schema
ChatGPTRe-runs the saved blend's GetSchema workflow so its schema cache in cloud storage is rebuilt. Use this when a query_data(blend_id=...) (or any saved-blend execution — including a scheduled BigQuery export) fails with an error mentioning that the blend's schema was not warmed up. Symptoms: - query_data(blend_id=…) raises BlendSchemaNotWarmedError, or any saved-blend execution surfaces a "blend has no cached schema" message. - A blend that ran fine yesterday now errors out without any other change (typical after a 3P token expiry: re-auth, then call this). - A blend created via the API or migrated in from another environment has never been queried. Calling this is idempotent and cheap — it overwrites the cache entry with a fresh resolution against the saved blend's account set.
resume_blend_schedule
ChatGPTResumes a previously paused blend-to-destination schedule. Idempotent.
share_report
ChatGPTChange a report's visibility / allowlist without redeploying. The deployed Worker reads visibility live from Porter on every request, so additions and revocations take effect on the next page load.
start_user_authorization
ChatGPTReturns the canonical Porter authorization URL pre-focused on a component. Format: https://<env>-app.portermetrics.com/porter-auth?component=<slug>&agent=<slug> Works for any connector or destination: Facebook Ads, Google Ads, TikTok, Shopify, BigQuery target (OAuth path), … The user opens it; Porter's UI runs the OAuth flow, creates the SourceUser / TargetUser, and on completion short-circuits the post-flow screen to "Back to <Agent>" because the URL carries the agent slug. For BigQuery there is also a service-account path that does NOT use this URL — see connect_bigquery(auth_mode="service_account", …) instead. Picking the right component_name: Slugs almost always include a type suffix — passing the bare platform name will fail validation. Common gotchas: - Instagram → instagram-insights (NOT instagram) - Facebook / Meta → facebook-ads, facebook-insights, or facebook-public-data (NOT facebook) - Google → google-ads, google-analytics-4, google-my-business, google-search-console, google-dv360 (NOT google) - TikTok → tiktok-ads or tiktok-insights (NOT tiktok) - LinkedIn → linkedin-ads or linkedin-pages (NOT linkedin) - X / Twitter → twitter-ads If unsure, call list_data_sources() first to see every valid slug. An invalid slug raises a ValueError whose message includes "did you mean" suggestions plus the full catalog, so it is safe to retry.
trigger_blend_now
ChatGPTConvenience wrapper: fires a one-shot run of the blend's schedule given only the blend id. The agent doesn't need to know the schedule id — addressing by blend id is the natural shape for "refresh this blend's BigQuery table now". Same success semantics as trigger_blend_schedule_now: status="successful" ⇒ run accepted and writing to the destination. Errors clearly if the blend has no schedule attached (in that case ask the user whether they want to set one up via create_blend_schedule_to_bigquery). Do NOT mention "schedule" or "BlendQuery" when explaining the result — the user thinks of this as "run my blend now".
trigger_blend_schedule_now
ChatGPTFires a one-shot run of a scheduled blend right now, independent of the cron and of whether the schedule is paused. Equivalent to clicking "Run now" in Porter's UI. Interpreting the response: status="successful" means the worker accepted the run and is writing to the destination. status="Error" or any non-success means the run did NOT happen — show the error to the user and stop. On success do NOT tell the user about Temporal workflows, schedules or payloads — just confirm that a fresh run is in flight and that the destination table will reflect the latest data. Note: list_blend_executions(blend_id) may take a few seconds to surface this run (the worker writes the history row after the destination write). A quick re-poll is OK; the trigger response is the real signal of acceptance.
update_report
ChatGPTUpdate an existing report — re-bundles and redeploys the Cloudflare Worker. The deployment URL stays stable across updates (the slug is preserved). A new ReportVersion row is created so the previous version can be inspected later via get_report. All four fields (config + frontend_html/css/js) are required together right now; metadata-only changes (rename) and config-only changes will come later. For surgical edits to an existing report, call get_report(report_id) first to read the current config + frontend_html/css/js, modify only what the user asked for, and pass the full updated bundle back here. Do NOT regenerate from scratch when patching — you may revert fixes already made. For sharing changes (visibility / allowed_emails) use share_report instead — that doesn't require a redeploy.