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:
- A model has a has-many relationship (like
Orderhaving manyOrderLinerecords) - 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:
{
"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.
| Field | Type | Description |
|---|---|---|
id | string | Unique identifier for the group |
title | string | Section heading displayed above the embedded tool |
display_order | number | Position among other groups (lower numbers appear first) |
visible | boolean | Whether the group is shown |
deleted | boolean | Remove an auto-generated embedded tool group |
tools | array | List 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
| Field | Type | Description |
|---|---|---|
tool_id | string | ID of the list tool to embed |
data_mapping | array | Maps parent values to the embedded tool's filter inputs |
title | string | Optional 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:
{
"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.
| Scenario | Use embedded tools | Use 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
{
"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.