{
  "access": "public",
  "type": "reference",
  "format": "markdown",
  "title": "Frame Tools Reference",
  "chunked": true,
  "url": "https://library.datagrout.ai/frame-tools",
  "summary": "Reshape, query, and combine lists of records — a columnar data operations layer for in-flight data.",
  "content_markdown": "# Frame Tools\n\nReshape, query, and combine lists of records — a columnar data operations layer for in-flight data.\n\nFrame tools are pure deterministic operations on lists of record maps. There are no external calls, no AI generation, and no credit cost. Every response includes a deterministic receipt under `_meta.datagrout`. Frame tools compose naturally with each other and with the Math suite via `flow.into`, and accept `cache_ref` outputs from auto-paginated integration tools so you can operate on large datasets without re-transmitting them. All Frame tools are available at `data-grout@1/frame.*@1`.\n\n---\n\n## `frame.select@1`\n\nKeep, rename, or drop columns from a list of records.\n\n### Parameters\n\n| Parameter | Type | Required | Default | Description |\n|-----------|------|----------|---------|-------------|\n| `payload` | array | conditionally | -- | List of record maps. Provide either `payload` or `cache_ref`, not both |\n| `cache_ref` | string | conditionally | -- | Cache reference from a prior auto-paginated tool call |\n| `columns` | array \\| object | no | -- | Fields to keep. A list of strings keeps only those fields; a list of specs like `{ \"field\": \"name\", \"as\": \"customer_name\" }` selects and renames in one pass; a `{ \"old_name\": \"new_name\" }` map also renames while selecting |\n| `drop` | array | no | -- | Field names to remove. Applied after `columns` if both are given |\n\n### Example: keep specific columns\n\n```json\n{\n  \"name\": \"data-grout@1/frame.select@1\",\n  \"arguments\": {\n    \"payload\": [\n      { \"name\": \"Alice\", \"email\": \"alice@acme.com\", \"role\": \"admin\", \"internal_id\": 1 },\n      { \"name\": \"Bob\", \"email\": \"bob@acme.com\", \"role\": \"user\", \"internal_id\": 2 }\n    ],\n    \"columns\": [\"name\", \"email\", \"role\"]\n  }\n}\n```\n\nResponse:\n\n```json\n{\n  \"records\": [\n    { \"name\": \"Alice\", \"email\": \"alice@acme.com\", \"role\": \"admin\" },\n    { \"name\": \"Bob\", \"email\": \"bob@acme.com\", \"role\": \"user\" }\n  ],\n  \"count\": 2\n}\n```\n\n### Example: rename columns\n\n```json\n{\n  \"name\": \"data-grout@1/frame.select@1\",\n  \"arguments\": {\n    \"payload\": [ \"...\" ],\n    \"columns\": { \"name\": \"customer_name\", \"email\": \"contact_email\" }\n  }\n}\n```\n\n### Example: mixed select + rename specs\n\n```json\n{\n  \"name\": \"data-grout@1/frame.select@1\",\n  \"arguments\": {\n    \"payload\": [ \"...\" ],\n    \"columns\": [\n      { \"field\": \"name\", \"as\": \"customer_name\" },\n      { \"field\": \"email\" },\n      \"status\"\n    ]\n  }\n}\n```\n\n---\n\n## `frame.filter@1`\n\nFilter rows using declarative predicate conditions. All conditions in the `where` array must match (AND logic).\n\n### Parameters\n\n| Parameter | Type | Required | Default | Description |\n|-----------|------|----------|---------|-------------|\n| `payload` | array | conditionally | -- | List of record maps |\n| `cache_ref` | string | conditionally | -- | Cache reference from a prior paginated call |\n| `where` | array | yes | -- | List of condition objects. Each has `field`, `op`, and (for most ops) `value` |\n\n### Filter operators\n\n| Operator | Description |\n|----------|-------------|\n| `eq` / `neq` | Equals / not equals |\n| `gt` / `gte` / `lt` / `lte` | Numeric comparisons |\n| `in` / `not_in` | Value is in (or not in) a list |\n| `contains` | String contains substring |\n| `starts_with` / `ends_with` | String prefix / suffix match |\n| `is_null` / `not_null` | Null check (no `value` needed) |\n\n### Example\n\n```json\n{\n  \"name\": \"data-grout@1/frame.filter@1\",\n  \"arguments\": {\n    \"payload\": [ \"...\" ],\n    \"where\": [\n      { \"field\": \"status\", \"op\": \"eq\", \"value\": \"active\" },\n      { \"field\": \"amount\", \"op\": \"gte\", \"value\": 1000 }\n    ]\n  }\n}\n```\n\nResponse:\n\n```json\n{\n  \"records\": [ \"...matched rows...\" ],\n  \"count\": 14,\n  \"filtered\": 38\n}\n```\n\n---\n\n## `frame.sort@1`\n\nSort records by one or more fields with per-field direction control.\n\n### Parameters\n\n| Parameter | Type | Required | Default | Description |\n|-----------|------|----------|---------|-------------|\n| `payload` | array | conditionally | -- | List of record maps |\n| `cache_ref` | string | conditionally | -- | Cache reference |\n| `by` | string or array | yes | -- | A single field name (string) or a list of sort specs. Each spec is a map with `field` (string) and optional `order`/`dir` (`\"asc\"` or `\"desc\"`, default `\"asc\"`). Bare strings in the list use the top-level `order` param. |\n| `order` | string | no | `\"asc\"` | Top-level sort direction applied to bare-string entries in `by`. Aliases: `dir`, `direction`. Ignored for spec objects that define their own `order`. |\n\n### Example\n\n```json\n{\n  \"name\": \"data-grout@1/frame.sort@1\",\n  \"arguments\": {\n    \"payload\": [ \"...\" ],\n    \"by\": [\n      { \"field\": \"revenue\", \"dir\": \"desc\" },\n      { \"field\": \"name\", \"dir\": \"asc\" }\n    ]\n  }\n}\n```\n\nResponse:\n\n```json\n{\n  \"records\": [ \"...sorted rows...\" ],\n  \"count\": 52\n}\n```\n\n---\n\n## `frame.group@1`\n\nGroup rows by one or more fields and aggregate other fields. Returns one record per group.\n\n### Parameters\n\n| Parameter | Type | Required | Default | Description |\n|-----------|------|----------|---------|-------------|\n| `payload` | array | conditionally | -- | List of record maps |\n| `cache_ref` | string | conditionally | -- | Cache reference |\n| `by` | string \\| array | yes | -- | Field name or list of field names to group by |\n| `agg` | array | no | -- | List of aggregation specs. Each has `field`, `op`, and optional `as` (output column name) |\n\n### Aggregation operators\n\n| Operator | Description |\n|----------|-------------|\n| `sum` | Sum of numeric values |\n| `mean` | Arithmetic mean |\n| `count` | Row count per group |\n| `count_distinct` | Count of distinct values |\n| `min` / `max` | Minimum / maximum value |\n| `first` / `last` | First or last value in group order |\n| `list` | Collect all values into an array |\n\n### Example\n\n```json\n{\n  \"name\": \"data-grout@1/frame.group@1\",\n  \"arguments\": {\n    \"payload\": [\n      { \"customer\": \"Acme\", \"amount\": 500, \"status\": \"paid\" },\n      { \"customer\": \"Globex\", \"amount\": 300, \"status\": \"pending\" },\n      { \"customer\": \"Acme\", \"amount\": 200, \"status\": \"paid\" }\n    ],\n    \"by\": \"customer\",\n    \"agg\": [\n      { \"field\": \"amount\", \"op\": \"sum\", \"as\": \"total\" },\n      { \"field\": \"amount\", \"op\": \"count\", \"as\": \"invoice_count\" }\n    ]\n  }\n}\n```\n\nResponse:\n\n```json\n{\n  \"records\": [\n    { \"customer\": \"Acme\", \"total\": 700, \"invoice_count\": 2 },\n    { \"customer\": \"Globex\", \"total\": 300, \"invoice_count\": 1 }\n  ],\n  \"count\": 2,\n  \"groups\": 2\n}\n```\n\n---\n\n## `frame.pivot@1`\n\nReshape a long dataset into a wide format. Distinct values of one column become new column headers.\n\n### Parameters\n\n| Parameter | Type | Required | Default | Description |\n|-----------|------|----------|---------|-------------|\n| `payload` | array | conditionally | -- | List of record maps in long format |\n| `cache_ref` | string | conditionally | -- | Cache reference |\n| `index` | string | yes | -- | Field whose values form the row identity |\n| `columns` | string | yes | -- | Field whose distinct values become the new column headers |\n| `values` | string | yes | -- | Field whose values fill the new columns |\n| `agg` | string | no | `\"first\"` | Aggregation to apply when multiple rows map to the same cell: `\"first\"`, `\"last\"`, `\"sum\"`, `\"mean\"`, `\"count\"`, `\"list\"` |\n\n### Example\n\n```json\n{\n  \"name\": \"data-grout@1/frame.pivot@1\",\n  \"arguments\": {\n    \"payload\": [\n      { \"region\": \"North\", \"quarter\": \"Q1\", \"revenue\": 42000 },\n      { \"region\": \"North\", \"quarter\": \"Q2\", \"revenue\": 48000 },\n      { \"region\": \"South\", \"quarter\": \"Q1\", \"revenue\": 31000 },\n      { \"region\": \"South\", \"quarter\": \"Q2\", \"revenue\": 35000 }\n    ],\n    \"index\": \"region\",\n    \"columns\": \"quarter\",\n    \"values\": \"revenue\"\n  }\n}\n```\n\nResponse:\n\n```json\n{\n  \"records\": [\n    { \"region\": \"North\", \"Q1\": 42000, \"Q2\": 48000 },\n    { \"region\": \"South\", \"Q1\": 31000, \"Q2\": 35000 }\n  ],\n  \"count\": 2,\n  \"pivot_columns\": [\"Q1\", \"Q2\"]\n}\n```\n\n---\n\n## `frame.slice@1`\n\nPaginate or take a top-N subset of records by offset and limit.\n\n### Parameters\n\n| Parameter | Type | Required | Default | Description |\n|-----------|------|----------|---------|-------------|\n| `payload` | array | conditionally | -- | List of record maps |\n| `cache_ref` | string | conditionally | -- | Cache reference |\n| `offset` | integer | no | `0` | Number of records to skip |\n| `limit` | integer | no | `25` | Maximum number of records to return |\n\n### Example\n\n```json\n{\n  \"name\": \"data-grout@1/frame.slice@1\",\n  \"arguments\": {\n    \"cache_ref\": \"pg_abc123\",\n    \"offset\": 50,\n    \"limit\": 25\n  }\n}\n```\n\nResponse:\n\n```json\n{\n  \"records\": [ \"...25 records...\" ],\n  \"count\": 25,\n  \"offset\": 50,\n  \"total\": 842\n}\n```\n\n---\n\n## `frame.join@1`\n\nJoin two lists of records on one or more shared key fields. For conflicting non-key fields, the left value wins and the right field is prefixed with `right_`.\n\n### Parameters\n\n| Parameter | Type | Required | Default | Description |\n|-----------|------|----------|---------|-------------|\n| `left` | array | conditionally | -- | Left-side record list. Provide `left` or `left_cache_ref` |\n| `right` | array | conditionally | -- | Right-side record list. Provide `right` or `right_cache_ref` |\n| `left_cache_ref` | string | conditionally | -- | Cache reference for left-side data |\n| `right_cache_ref` | string | conditionally | -- | Cache reference for right-side data |\n| `on` | string \\| array | yes | -- | Key field name or list of key field names to join on |\n| `type` | string | no | `\"inner\"` | Join type: `\"inner\"`, `\"left\"`, `\"right\"`, `\"outer\"` |\n\n### Example\n\n```json\n{\n  \"name\": \"data-grout@1/frame.join@1\",\n  \"arguments\": {\n    \"left\": [\n      { \"id\": \"c1\", \"name\": \"Acme Corp\", \"tier\": \"enterprise\" },\n      { \"id\": \"c2\", \"name\": \"Globex\", \"tier\": \"growth\" }\n    ],\n    \"right\": [\n      { \"id\": \"c1\", \"open_invoices\": 3, \"overdue_amount\": 4500 },\n      { \"id\": \"c3\", \"open_invoices\": 1, \"overdue_amount\": 800 }\n    ],\n    \"on\": \"id\",\n    \"type\": \"left\"\n  }\n}\n```\n\nResponse:\n\n```json\n{\n  \"records\": [\n    { \"id\": \"c1\", \"name\": \"Acme Corp\", \"tier\": \"enterprise\", \"open_invoices\": 3, \"overdue_amount\": 4500 },\n    { \"id\": \"c2\", \"name\": \"Globex\", \"tier\": \"growth\", \"open_invoices\": null, \"overdue_amount\": null }\n  ],\n  \"count\": 2\n}\n```\n\n---\n\n## Using `cache_ref` with frame tools\n\nEvery tool call result is now cached and assigned a `cache_ref` at `_meta.datagrout.cache_ref`. Frame tools can reference this ref to operate on cached data without re-transmitting it through the LLM context. See also the `data.*` tools for general JSON structure manipulation with cache_ref support.\n\n```json\n{\n  \"name\": \"data-grout@1/flow.into@1\",\n  \"arguments\": {\n    \"plan\": [\n      {\n        \"id\": \"fetch\",\n        \"tool\": \"quickbooks@1/get-all-invoices@1\",\n        \"args\": { \"limit\": 10000 }\n      },\n      {\n        \"id\": \"group\",\n        \"tool\": \"data-grout@1/frame.group@1\",\n        \"args\": {\n          \"cache_ref\": \"$fetch._cache_ref\",\n          \"by\": \"customer_name\",\n          \"agg\": [\n            { \"field\": \"amount\", \"op\": \"sum\", \"as\": \"total_revenue\" },\n            { \"field\": \"id\", \"op\": \"count\", \"as\": \"invoice_count\" }\n          ]\n        }\n      },\n      {\n        \"id\": \"top10\",\n        \"tool\": \"data-grout@1/frame.sort@1\",\n        \"args\": {\n          \"payload\": \"$group.records\",\n          \"by\": [{ \"field\": \"total_revenue\", \"dir\": \"desc\" }]\n        }\n      },\n      {\n        \"id\": \"display\",\n        \"tool\": \"data-grout@1/frame.slice@1\",\n        \"args\": { \"payload\": \"$top10.records\", \"limit\": 10 }\n      }\n    ]\n  }\n}\n```\n"
}