本文へ移動
cccskills
無料GitHub で公開日本語紹介

data-programs

一度きりのデータ分析を、更新可能なダッシュボード用データソースとして保存します。取得・結合・集計の処理を試してから登録し、パネルで再利用できます。

原文原文の説明を見る

Save a run-code fetch/join/aggregate script as a refreshable data source panels bind to. Use when a provider-api-request or run-code analysis should become a live panel.

BuilderIO/agent-nativeスター 7,525(GitHub 7,525 + cccskills 0)
連携ツールHubSpot

実行スコア 未検証

インストール方法を見る

こんなときに便利

  • 繰り返し見る分析を保存したいとき
  • 複数サービスのデータを結合したいとき
  • 独自条件の集計をパネル表示したいとき
  • 保存前に分析スクリプトを試したいとき

日本語での紹介

できること

一度きりのデータ分析で使ったスクリプトを、名前付きのデータソースとして保存し、ダッシュボードのパネルに結び付けます。データの取得、絞り込み、複数のデータ源の結合、集計を行うJavaScriptを、まず通常の分析として試し、期待する行が得られたら保存します。登録時にはコードが既定パラメーターで試しに実行され、結果の行数や列、サンプルを確認できます。保存したプログラムは更新間隔または手動更新を設定でき、パネルでは表やグラフとして表示できます。保存前のプレビューや、必要に応じた再実行にも対応します。

こんなときに便利

定期的に見返す独自の集計や、標準のデータ取得機能では表現しにくい条件での分析に向いています。異なるサービスのデータを結び付けた結果を、チャットへの一度きりの回答ではなく、チームのダッシュボードで共有したい場合にも使えます。

使い方の例

  • 「この分析スクリプトを保存して、更新できるダッシュボード用データにして」
  • 「この条件の顧客一覧を試しに取得して、パネルに使える形で登録して」
  • 「保存済みのデータプログラムを手動で再実行して」

注意点

実行にはrun-codeで利用できるデータ取得環境が必要です。コードは閲覧者自身の認証情報で動き、共有プログラムは組織内に限って共有できます。実行ごとに出力行数やサイズ、更新間隔に上限があります。

この紹介文は、公開されている SKILL.md をもとに AI(OpenAI Codex)が作成しました。正確な仕様は下の原文を確認してください。

含まれるファイル(1)

  • SKILL.md20.5 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

Data Programs

A data program is a named, stored, agent-authored JS script that fetches, joins, filters, or aggregates provider or app data, and is rendered by a dashboard panel. It is the generic answer to "I can query this once in chat — now make it a panel other people can see, that refreshes on its own."

There is no new credential machinery here. A data program is exactly a run-code script (same providerFetch / providerFetchAll / providerSearchAll / appAction / workspace* globals you already use for ad-hoc analysis) plus two things: a stored, named identity, and a small emit(rows, schema) contract so the result can be cached and charted. Read provider-api first if you have not already — data programs assume you know provider-api-catalog / provider-api-docs / provider-api-request.

When to reach for this vs. a canned action

  • The common case (won/lost by pipeline, a standard date-range rollup) already has a first-class action (hubspot-deals, etc.) — use that.
  • A one-time chat question — just query it with run-code or provider-api-request and answer directly. Don't save a program for something you'll never look at again.
  • A cohort needs an arbitrary property filter, an IN-search over custom values, or a join against a second provider, and the user (or a dashboard) will want to see it again with fresh data — that's a data program.

Authoring workflow

  1. Prototype in chat first. Write the fetch/join/aggregate logic as a normal run-code script and confirm it returns the rows you expect.
  2. Wrap it to call emit(rows, schema) exactly once at the end instead of returning/printing the result (see contract below).
  3. Save it with save-data-program:
    save-data-program(
      name: "risk-meeting-cohort",      // stable slug, unique per your account
      title: "Risk Meeting — HubSpot cohort",
      description: "...",
      code: "<the run-code script text>",
      defaultParams: { riskStatuses: ["Churn Risk"] },  // optional
      refreshMode: "ttl",               // "ttl" | "manual"
      refreshTtlMs: 900000,             // optional, floor 60000
      background: false,                // true for slow multi-minute joins
    )
    
    This dry-runs the code with defaultParams before persisting and rejects the save with a structured error if it fails — a broken program is never stored. On success it returns { programId, rowCount, columns, sampleRows }: use that as your proof the program actually produces the rows you expect before telling the user it's ready.
  4. Bind a dashboard panel to it. Panels reference programs through the "program" source; the panel's sql field carries a JSON descriptor instead of a query string:
    { "programId": "dp_...", "params": { "riskStatuses": ["Churn Risk"] } }
    
    The panel renders through the exact same chart/table components as every other panel source — it only ever receives { rows, schema }.
  5. Iterate without re-saving with preview-data-program(code, params?) — same dry-run path, no persisted row. Use this while tuning a script before calling save-data-program again.
  6. Refresh on demand with run-data-program(programId, params?, forceRefresh?, includeRows?) — this is what the agent calls to get a compact proof-of-result (rowCount, columns, sampleRows) without waiting for a panel view. Pass includeRows: true only when you need the full (capped) row set back in context.

The sandbox surface

Inside a data program you get exactly the run-code globals, plus one addition:

  • providerFetch(provider, path, init?), providerFetchAll(...), providerSearchAll(...) — the same generic provider-access helpers used for ad-hoc analysis. See provider-api for the pagination/search option shapes.
  • appAction(name, params), webFetch, workspace* Resources helpers — same as run-code.
  • params — a frozen global object: the params passed into this run (defaultParams merged by the caller, or whatever was passed to run-data-program / the panel descriptor). Read it, never mutate it.
  • emit(rows, schema?) — call this exactly once, at the end, with your result. A second call throws. schema is optional — { name: string, type: string }[] — inferred automatically from the first 50 rows when omitted (a column is "json" if rows disagree on primitive type, otherwise "number" / "string" / "boolean"). console.log remains completely free for debugging; it never interferes with emit() parsing and is captured separately as a truncated log tail on the run record.

Caps enforced on every run (not configurable per-program):

LimitValue
Max emitted rows10,000 (rows beyond this are dropped, truncated: true)
Max emitted result size4 MiB (rows dropped from the end to fit; a single row over 4 MiB is a hard result_too_large failure — nothing to safely keep)
Max active programs per app200 (archive unused ones to free room)
Minimum refresh TTL60,000 ms
Run rows kept per (program, params)5 most recent (older ones are pruned automatically)

Truncation is always honest: a partial result comes back with truncated: true, never a silent drop.

Caching and refresh model

Every (programId, paramsHash) pair (params are canonicalized and hashed, so equivalent param objects share a cache entry) has its own run history. What happens on each call to runDataProgram / a panel view:

SituationBehavior
Fresh cache hit (younger than refreshTtlMs, or refreshMode: "manual" with any prior success)Returns cached rows instantly, cacheHit: true
Cache is older than the TTLRe-executes synchronously (panel views get a 25s budget; agent/manual calls get 120s), replacing the cache on success
refreshMode: "manual"Never auto-refreshes; only forceRefresh: true (or manual_refresh from the UI) re-runs it
No cache yetExecutes synchronously like a normal first fetch
background: true program, no fresh cacheServes the last good run immediately with stale: true and enqueues a durable background execution (10-minute budget); the next call finalizes it if it has completed
Background program, execution still running, no prior successbackground_pending failure — the panel shows an explicit "running in background" error card, never a blank chart
Execution fails (timeout, sandbox error, bad emit() shape)Failure result with a structured error code; if a previous successful run exists, it is attached as lastGoodRun so the panel can stale-serve instead of going blank
Program was archived (delete-data-program)Explicit archived failure — panels show "This data program was archived", never a silent blank

Use background: true only for programs that routinely take longer than the foreground budget (multi-provider joins over large cohorts, deep pagination). Everything else should stay foreground — it is simpler to reason about and serves fresh data faster.

Error codes

CodeMeaningWhat to do
program_not_foundNo program with that idCheck the id via list-data-programs
access_deniedCaller can't see this programDon't leak existence; ask the owner to share it
archivedProgram was soft-deletedRepoint the panel or unarchive by resaving under the same name
timeoutDidn't finish within the run's budgetReduce scope, page in fewer items, or mark background: true
emit_missingScript never called emit(...)Add the emit(rows, schema?) call at the end
emit_shape_invalidemit() args weren't (array-of-plain-objects, optional-schema-array)Fix the shape; check for a stray second emit() call
result_too_largeA single row (or the whole result) exceeds the byte capTrim large string fields before emitting
sandbox_errorUncaught exception in the scriptRead the thrown message; guard optional provider fields
run_code_unavailableThe run-code module isn't available in this buildNot something a program author can fix
background_pendingBackground program has no result yetWait and retry, or check lastGoodRun if present

Every failure is a structured { code, message }, never a bare string — treat code as the thing to branch on, message as the thing to show a human.

Security notes

  • Credentials are always the viewer's, never the author's. When a shared program's panel is viewed, providerFetch inside it resolves auth using the viewing user's own configured provider credentials — not the program author's. A teammate missing a HubSpot key sees a HubSpot auth error on that panel, not someone else's data.
  • Only share programs you trust. Because execution uses the viewer's credentials, only bind to or view a shared data program if you trust its code — it runs with your own access, exactly like opening someone else's extension.
  • Data programs are org-internal only, never public. Unlike dashboards, a data program can never be shared publicly — sharing is restricted to members of your org. This is a hard invariant, not a default.
  • Raw provider tokens never reach the model or the browser. The only thing that ever leaves the sandbox is the { rows, schema } your emit() call produces — the same trust boundary as every other dashboard panel source.
  • The actions that execute or mutate stored code (save-data-program, preview-data-program, run-data-program, delete-data-program) are not callable from the sandboxed extensions/iframe bridge — only from the agent or the server-rendered panel path.

Worked example: Risk Meeting (HubSpot x Pylon)

This is the reference example installed by the ensure-risk-meeting-dashboard action (templates/analytics/actions/ensure-risk-meeting-dashboard.ts), which idempotently upserts both programs below and installs the built-in two-panel "Risk Meeting" dashboard. It demonstrates the three capabilities a hardcoded action can't anticipate for every customer: an arbitrary HubSpot property IN-search, a batched multi-hop association join, and a join against a second, unrelated provider.

risk-meeting-cohort.js — HubSpot deals in a configurable risk_status cohort, resolved to their company domain, joined against Pylon account sentiment by domain:

// params: { riskStatuses?: string[], enterpriseOnly?: boolean }
const DEFAULT_RISK_STATUSES = [
  "On the Radar",
  "Churn Risk",
  "Confirmed Churn",
  "No Save Attempted",
];
const riskStatuses =
  Array.isArray(params.riskStatuses) && params.riskStatuses.length > 0
    ? params.riskStatuses
    : DEFAULT_RISK_STATUSES;
const HIGH_RISK_SENTIMENTS = ["frustrated", "high_risk_detractor"];

function chunk(items, size) {
  const out = [];
  for (let i = 0; i < items.length; i += size)
    out.push(items.slice(i, i + size));
  return out;
}

async function main() {
  // 1. Arbitrary property IN-search + open-close-date filter — not
  //    expressible through the canned hubspot-deals filters.
  const dealSearch = await providerFetchAll(
    "hubspot",
    "/crm/v3/objects/deals/search",
    {
      method: "POST",
      body: {
        filterGroups: [
          {
            filters: [
              {
                propertyName: "risk_status",
                operator: "IN",
                values: riskStatuses,
              },
              {
                propertyName: "closedate",
                operator: "GT",
                value: String(Date.now()),
              },
            ],
          },
        ],
        properties: [
          "dealname",
          "risk_status",
          "risk_summary",
          "risk_category",
          "hs_next_step",
          "churn_notes",
          "total_contract_value",
          "customer_success_owner",
          "dealstage",
          "closedate",
        ],
        limit: 100,
      },
      itemsPath: "results",
      pagination: {
        cursorBodyPath: "after",
        nextCursorPath: "paging.next.after",
        maxPages: 20,
      },
    },
  );
  const deals = dealSearch.items || [];
  const dealIds = deals.map((d) => d && d.id).filter(Boolean);

  // 2. Batched deal -> company association (100 per call). The raw
  //    association rows never leave this program.
  const companyByDeal = {};
  for (const batch of chunk(dealIds, 100)) {
    if (batch.length === 0) continue;
    const assoc = await providerFetch(
      "hubspot",
      "/crm/v3/associations/deals/companies/batch/read",
      { method: "POST", body: { inputs: batch.map((id) => ({ id })) } },
    );
    for (const r of (assoc && assoc.results) || []) {
      const dealId = r && r.from && r.from.id;
      const companyId =
        r && Array.isArray(r.to) && r.to.length > 0 ? r.to[0].id : null;
      if (dealId && companyId) companyByDeal[dealId] = companyId;
    }
  }

  // 3. Batched company -> domain lookup.
  const companyIds = Array.from(new Set(Object.values(companyByDeal))).filter(
    Boolean,
  );
  const domainByCompany = {};
  for (const batch of chunk(companyIds, 100)) {
    if (batch.length === 0) continue;
    const companies = await providerFetch(
      "hubspot",
      "/crm/v3/objects/companies/batch/read",
      {
        method: "POST",
        body: { inputs: batch.map((id) => ({ id })), properties: ["domain"] },
      },
    );
    for (const r of (companies && companies.results) || []) {
      const domain =
        r && r.properties && r.properties.domain
          ? String(r.properties.domain).toLowerCase()
          : null;
      if (r && r.id && domain) domainByCompany[r.id] = domain;
    }
  }

  // 4. A second, unrelated provider — zero bespoke glue action required.
  const pylonAccounts = await providerFetchAll("pylon", "/accounts", {
    itemsPath: "data",
    pagination: {
      cursorParam: "cursor",
      nextCursorPath: "pagination.cursor",
      maxPages: 20,
    },
  });
  const sentimentByDomain = {};
  for (const account of pylonAccounts.items || []) {
    if (!account) continue;
    const domain =
      typeof account.domain === "string" ? account.domain.toLowerCase() : null;
    if (!domain) continue;
    if (
      params.enterpriseOnly &&
      account.account_profile !== "Enterprise Active Customer"
    )
      continue;
    if (account.general_sentiment)
      sentimentByDomain[domain] = account.general_sentiment;
  }

  // 5. Join + emit. csm_name is computed HERE — never baked into a generic
  //    hubspot-deals action.
  const rows = deals.map((deal) => {
    const props = (deal && deal.properties) || {};
    const dealId = deal && deal.id;
    const companyId = dealId ? companyByDeal[dealId] : null;
    const domain = companyId ? domainByCompany[companyId] : null;
    const sentiment = domain ? sentimentByDomain[domain] || null : null;
    return {
      deal_id: dealId || null,
      deal_name: props.dealname || null,
      risk_status: props.risk_status || null,
      risk_summary: props.risk_summary || null,
      risk_category: props.risk_category || null,
      next_step: props.hs_next_step || null,
      churn_notes: props.churn_notes || null,
      arr: Number(props.total_contract_value || 0),
      csm_name: props.customer_success_owner || null,
      dealstage: props.dealstage || null,
      closedate: props.closedate || null,
      domain: domain || null,
      pylon_sentiment: sentiment,
      cross_source_flag: Boolean(
        domain && HIGH_RISK_SENTIMENTS.includes(sentiment),
      ),
    };
  });

  emit(rows, [
    { name: "deal_id", type: "string" },
    { name: "deal_name", type: "string" },
    { name: "risk_status", type: "string" },
    { name: "risk_summary", type: "string" },
    { name: "risk_category", type: "string" },
    { name: "next_step", type: "string" },
    { name: "churn_notes", type: "string" },
    { name: "arr", type: "number" },
    { name: "csm_name", type: "string" },
    { name: "dealstage", type: "string" },
    { name: "closedate", type: "string" },
    { name: "domain", type: "string" },
    { name: "pylon_sentiment", type: "string" },
    { name: "cross_source_flag", type: "boolean" },
  ]);
}

await main();

risk-meeting-pylon-early-warning.js — the complementary "support signal outrunning CRM signal" view: enterprise Pylon accounts already flagged at-risk by sentiment that have not yet shown up in the HubSpot cohort above (a same-shape HubSpot domain lookup, minus the Pylon domain set — see templates/analytics/seeds/data-programs/risk-meeting-pylon-early-warning.js for the full script). This is the third validation capability: excluding one provider's cohort from another's, entirely inside one program.

Both programs are installed and kept up to date by ensure-risk-meeting-dashboard — an idempotent action that upserts each program by its stable name and installs the read-only, built-in two-panel "Risk Meeting" dashboard (builtin-risk-meeting) whose panels use source: "program". Installing the dashboard requires no HubSpot/Pylon credentials up front; provider auth resolves per-viewer only when a panel is actually rendered or run-data-program is called.

Related skills

  • provider-api — the catalog/docs/request/staging actions a data program's providerFetch* calls are built on.
  • dashboard-management — creating and laying out the dashboard a program's panel lives on.
  • cross-source-analysis — identity-stitching discipline (stable id + email, de-duplication, provenance) for joins like the one above.
  • security — credential handling and access-scoping invariants this primitive relies on.

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

コメント

ログインなしで書き込めます。使ってみた感想、質問、動かなかった環境などを気軽にどうぞ。

まだコメントはありません。最初のコメントを書いてみませんか。

送信するとすぐに公開されます。メールアドレスや電話番号などの個人情報は書かないでください。URL は 2 つまでです。

0 / 1000 文字

このフォームは reCAPTCHA で保護されており、Google のプライバシーポリシーと利用規約が適用されます。

同じリポジトリのスキル

a2a-assets

無料概要あり

複数のアプリやMCPホストからブランド画像・動画を生成、調整、書き出し、挿入する作業を支援します。ライブラリ選択や修正依頼も扱います。

  • スライド用のブランド画像を作りたいとき
  • 既存素材をフィードバックに合わせて直したいとき
  • 動画素材を生成して書き出したいとき
BuilderIO/agent-native7,5252026年10月26日 更新

account-health

無料概要あり

特定の顧客企業について、契約と製品利用の数値を照らし合わせ、根拠や不足情報を明記した健全性レポートをまとめます。

  • 顧客の健全性を確認したいとき
  • 契約席数と実利用を比べたいとき
  • 更新前に利用状況を整理したいとき
BuilderIO/agent-native7,5252026年10月26日 更新

actions

無料概要あり

アプリの操作をエージェントと画面の両方から呼び出せるアクションとして実装し、API連携や画面のデータ取得を一貫した形で追加するスキルです。

  • 新しいアプリ操作を追加したいとき
  • 外部APIと連携したいとき
  • 画面からデータを取得したいとき
BuilderIO/agent-native7,5252026年10月26日 更新

adding-a-feature

無料概要あり

新機能をUI・エージェント用アクション・説明資料・画面状態の4領域でそろえ、利用者とエージェントのどちらからも一貫して使えるようにするチェックリストです。

  • 新機能追加の作業範囲を整理したいとき
  • UIとエージェント操作をそろえたいとき
  • 画面状態の同期漏れを防ぎたいとき
BuilderIO/agent-native7,5252026年10月26日 更新

adhoc-analysis

無料概要あり

複数のデータソースを照合して、傾向や指標、根拠をまとめた分析結果を作成します。必要に応じて、再実行できるダッシュボードとして保存できます。

  • 商談と通話記録を照合したいとき
  • 顧客の利用状況を横断して調べたいとき
  • 繰り返し更新できる分析を保存したいとき
BuilderIO/agent-native7,5252026年10月26日 更新

admin-surfaces

無料概要あり

Analyticsの管理画面で、機能フラグの状態確認と切り替え、ダッシュボード利用状況の調査、接続先アプリのデータベース管理を行います。

  • アプリごとの機能フラグを確認・変更したいとき
  • ダッシュボードの所有者や利用状況を調べたいとき
  • 接続済みアプリのテーブルを管理したいとき
BuilderIO/agent-native7,5252026年10月26日 更新

BuilderIO のスキルをすべて見る

このスキルの問題を報告する