Embedded Tools

Embedded Tools

Embedded tools display related data inline within a parent tool. When viewing an order, you might want to see its order lines directly on the same page rather than navigating away. Embedded tools make this possible by nesting list tools inside get tools.

How embedded tools work

When you have a model with a has-many relationship, Keel can automatically embed a list of related records within the parent's detail view. For example, viewing an Order can show all its OrderLine records inline.

model Order {
  fields {
    reference Text @unique
    customer Customer
    status OrderStatus
  }
 
  actions {
    get getOrder(id)
  }
}
 
model OrderLine {
  fields {
    order Order
    product Product
    quantity Number
    unitPrice Decimal
  }
 
  actions {
    list listOrderLines(order.id)
  }
}

With this schema, Keel automatically embeds listOrderLines within getOrder. When a user views an order, the order lines appear in a section below the order details.

Automatic embedding

Keel automatically creates embedded tools when:

  1. A model has a has-many relationship (like Order having many OrderLine records)
  2. The related model has a list action with the parent's ID as a filter input

The embedded list is automatically filtered to show only records belonging to the parent. The foreign key column (e.g., orderId) is hidden since it would be redundant.

Configuring embedded tools

While Keel generates embedded tools automatically, you can customise them through configuration files. Embedded tool configuration lives in the embedded_tools array of your tool configuration:

tools/get-order.json
{
  "id": "get-order",
  "action_name": "getOrder",
  "embedded_tools": [
    {
      "id": "order-lines",
      "title": "Order Lines",
      "display_order": 1,
      "visible": true,
      "tools": [
        {
          "action_link": {
            "tool_id": "list-order-lines",
            "data_mapping": [
              {
                "key": "$.where.order.id.equals",
                "path": { "path": "$.id" }
              }
            ]
          },
          "response_overrides": {
            "$.results[*].orderId": false
          }
        }
      ]
    }
  ]
}

Tool group options

Each embedded tool is organised into a tool group. A tool group is a named section that can contain one or more embedded tools.

FieldTypeDescription
idstringUnique identifier for the group
titlestringSection heading displayed above the embedded tool
display_ordernumberPosition among other groups (lower numbers appear first)
visiblebooleanWhether the group is shown
deletedbooleanRemove an auto-generated embedded tool group
toolsarrayList of embedded tool configurations

Embedded tool configuration

Each tool within a group has two parts: the action link defining which tool to embed and how to filter it, and response overrides controlling which columns to show.

{
  "action_link": {
    "tool_id": "list-order-lines",
    "data_mapping": [
      {
        "key": "$.where.order.id.equals",
        "path": { "path": "$.id" }
      }
    ]
  },
  "response_overrides": {
    "$.results[*].orderId": false
  }
}

Action link options

FieldTypeDescription
tool_idstringID of the list tool to embed
data_mappingarrayMaps parent values to the embedded tool's filter inputs
titlestringOptional label for the embedded tool

Data mapping

Data mapping connects the parent record's values to the embedded tool's inputs. This ensures the embedded list only shows related records.

{
  "data_mapping": [
    {
      "key": "$.where.order.id.equals",
      "path": { "path": "$.id" }
    }
  ]
}

Each mapping specifies:

  • key: The target input path on the embedded tool (typically a filter like $.where.order.id.equals)
  • path: The source path on the parent record (typically $.id)

Response overrides

Response overrides let you hide columns from the embedded list that would be redundant. When viewing order lines on an order page, showing the orderId column adds no value since every row has the same value.

{
  "response_overrides": {
    "$.results[*].orderId": false,
    "$.results[*].order.reference": false
  }
}

Set the field path to false to hide it. The path uses the format $.results[*].fieldName where [*] indicates the field applies to all rows in the list.

Response overrides only affect the embedded view. The same list tool used standalone will still show all columns.

Multiple embedded tools

You can embed multiple tool groups within a single parent tool. This is useful when a record has several related lists:

tools/get-order.json
{
  "id": "get-order",
  "action_name": "getOrder",
  "embedded_tools": [
    {
      "id": "order-lines",
      "title": "Order Lines",
      "display_order": 1,
      "visible": true,
      "tools": [
        {
          "action_link": {
            "tool_id": "list-order-lines",
            "data_mapping": [
              {
                "key": "$.where.order.id.equals",
                "path": { "path": "$.id" }
              }
            ]
          },
          "response_overrides": {
            "$.results[*].orderId": false
          }
        }
      ]
    },
    {
      "id": "shipments",
      "title": "Shipments",
      "display_order": 2,
      "visible": true,
      "tools": [
        {
          "action_link": {
            "tool_id": "list-shipments",
            "data_mapping": [
              {
                "key": "$.where.order.id.equals",
                "path": { "path": "$.id" }
              }
            ]
          },
          "response_overrides": {
            "$.results[*].orderId": false
          }
        }
      ]
    },
    {
      "id": "activity",
      "title": "Activity Log",
      "display_order": 3,
      "visible": true,
      "tools": [
        {
          "action_link": {
            "tool_id": "list-order-events",
            "data_mapping": [
              {
                "key": "$.where.order.id.equals",
                "path": { "path": "$.id" }
              }
            ]
          }
        }
      ]
    }
  ]
}

This configuration shows order lines, shipments, and activity logs all on the same order detail page, each in their own section.

When to use embedded vs linked tools

Embedded tools and linked tools both connect related data, but they serve different purposes.

ScenarioUse embedded toolsUse linked tools
Child records always viewed with parent
Quick overview of related data
Large datasets that need full filtering
Navigation between independent records
Master-detail workflows
Loosely related data

Use embedded tools when:

  • The related records are closely tied to the parent (order lines belong to an order)
  • Users typically need to see both together
  • The list of related records is manageable in size

Use linked tools when:

  • Users need full table functionality with sorting and filtering
  • The relationship is more of a reference than ownership
  • You want to navigate between records rather than view inline

Example: Order with embedded order lines

Here's a complete example showing an order management schema with embedded order lines:

Schema

model Order {
  fields {
    reference Text @unique
    customer Customer
    status OrderStatus
    placedAt Timestamp
    notes Text?
  }
 
  actions {
    get getOrder(id)
    list listOrders(reference?, customer.id?, status?)
    create createOrder() with (reference, customer.id, status, placedAt, notes?)
    update updateOrder(id) with (status?, notes?)
    delete deleteOrder(id)
  }
}
 
model OrderLine {
  fields {
    order Order
    product Product
    quantity Number
    unitPrice Decimal
  }
 
  actions {
    get getOrderLine(id)
    list listOrderLines(order.id)
    create createOrderLine() with (order.id, product.id, quantity, unitPrice)
    update updateOrderLine(id) with (quantity?, unitPrice?)
    delete deleteOrderLine(id)
  }
}
 
enum OrderStatus {
  Draft
  Placed
  Processing
  Shipped
  Delivered
  Cancelled
}

Configuration

tools/get-order.json
{
  "id": "get-order",
  "action_name": "getOrder",
  "name": "View Order",
  "title": "Order #{{$.reference}}",
  "help_text": "View order details and manage order lines",
  "sections": [
    {
      "name": "details",
      "title": "Order Details",
      "display_order": 1,
      "visible": true
    }
  ],
  "response": {
    "$.reference": {
      "display_name": "Order Reference",
      "section_name": "details",
      "display_order": 1
    },
    "$.customer.name": {
      "display_name": "Customer",
      "section_name": "details",
      "display_order": 2
    },
    "$.status": {
      "section_name": "details",
      "display_order": 3
    },
    "$.placedAt": {
      "display_name": "Placed",
      "section_name": "details",
      "display_order": 4
    }
  },
  "display_layout": {
    "config": {
      "type": "RECORD",
      "recordConfig": {
        "progressIndicator": {
          "enabled": true,
          "stepperField": { "path": "$.status" },
          "steps": [
            { "displayOrder": 1, "value": ["Draft"] },
            { "displayOrder": 2, "value": ["Placed"] },
            { "displayOrder": 3, "value": ["Processing"] },
            { "displayOrder": 4, "value": ["Shipped"] },
            { "displayOrder": 5, "value": ["Delivered"] }
          ]
        }
      }
    }
  },
  "embedded_tools": [
    {
      "id": "lines",
      "title": "Order Lines",
      "display_order": 1,
      "visible": true,
      "tools": [
        {
          "action_link": {
            "tool_id": "list-order-lines",
            "data_mapping": [
              {
                "key": "$.where.order.id.equals",
                "path": { "path": "$.id" }
              }
            ]
          },
          "response_overrides": {
            "$.results[*].orderId": false,
            "$.results[*].order.reference": false
          }
        }
      ]
    }
  ],
  "entry_activity_actions": [
    {
      "tool_id": "update-order",
      "display_order": 1
    },
    {
      "tool_id": "create-order-line",
      "title": "Add Line",
      "as_dialog": true,
      "display_order": 2,
      "data_mapping": [
        {
          "key": "$.order.id",
          "path": { "path": "$.id" }
        }
      ]
    }
  ]
}

This configuration creates an order detail page that:

  • Shows order details in a dedicated section
  • Displays a progress indicator based on order status
  • Embeds the order lines list below the details
  • Hides the redundant order ID and reference columns from the embedded list
  • Provides an "Add Line" button that opens a dialog pre-filled with the order ID

Hiding embedded tools

To hide an automatically generated embedded tool group, set visible to false:

{
  "embedded_tools": [
    {
      "id": "order-lines",
      "visible": false
    }
  ]
}

This keeps the embedded tool in the configuration but hides it from the UI.

Removing embedded tools

To completely remove an auto-generated embedded tool rather than hiding it, use the deleted property:

{
  "embedded_tools": [
    {
      "id": "order-lines",
      "deleted": true
    }
  ]
}
⚠️

Removing an embedded tool group removes it entirely from the tool. If you only want to temporarily hide it, use visible: false instead.