When creating or updating a lookup field with lark-cli base +field-create/+field-update --json ... and type is lookup, you should read this guide first and only then add --i-have-read-guide to the command.
Do not proactively add --i-have-read-guide before reading this guide. Without it, the CLI will fail fast and direct you back to this guide.
Use Formula fields by default for cross-table references and aggregations. Only use Lookup fields when the user explicitly requests a Lookup field. Formula is a strict superset of Lookup — anything Lookup can do, Formula can do with a single expression.
When creating a lookup field, the Agent should:
lark-cli base +table-list --base-token <base> — returns items[].table_namelark-cli base +table-get --base-token <base> --table-id <table> — returns fields[]Key constraints:
+table-list / +table-getfrom table must be in the same BaseA Lookup field is defined by five fields:
| Field | Meaning | JSON key | Required |
|---|---|---|---|
| type | Must be "lookup" |
type |
Yes |
| from | Source table to pull data from | from |
Yes |
| select | Field in the source table to retrieve | select |
Yes |
| where | Filter conditions on the source table | where |
Yes (at least one condition) |
| aggregate | How to aggregate multiple matching records | aggregate |
No (default: raw_value) |
SQL analogy:
SELECT [select field]
FROM [from table]
WHERE [filter conditions]
GROUP BY [aggregate function]
Row-level matching (most important concept):
A Lookup field is computed row-by-row — for each row in the current table, it filters the source table to find "related" records. The filter defines what "related" means.
Current table row 1 → filter source table → matching records → select field → aggregate → result
Current table row 2 → filter source table → matching records → select field → aggregate → result
...
Rule: Whenever the current table and the source table have a row-level correspondence (matching by some field value), you must specify a filter.
Lookup and Link serve different purposes. Creating a Lookup does NOT require a Link field to exist first.
| Dimension | Link | Lookup | Formula |
|---|---|---|---|
| Purpose | Establish record relationships (read-write) | Pull and aggregate data from another table (read-only) | Compute values from expressions (read-only) |
| When to use | "link" / "associate" / "bind" two tables | "look up" / "reference" / "aggregate" / "count" from another table | Calculations, text manipulation, conditional logic |
Common mistake: Creating a Link field just to create a Lookup. If two tables share a matching text/number field, Lookup can match directly — no Link required.
Selection decision tree:
What does the user need?
├─ "Link"/"associate"/"bind" records between tables → Link
├─ "Look up"/"reference"/"aggregate"/"count" from another table → Lookup
│ ├─ Needs aggregation (sum/count/average)? → Lookup + aggregate
│ └─ Just reference a value? → Lookup (aggregate = null)
├─ Calculations/text manipulation within current table → Formula
└─ Access linked record's field → Prefer Lookup (more intuitive), or Formula chain access
You must provide a where with at least one condition. Improper conditions cause every row to pull all records from the source table.
filter condition:
field → must be a field in the FROM table (source table)
value → constant or reference to a field in the CURRENT table
With a Link field (most common): The match is between the Link field and the target table's primary field.
Link is in the source table → source.linkField matches current.primaryField
Link is in the current table → source.primaryField matches current.linkField
Without a Link field: Two tables share a field with the same meaning — match directly.
Each condition is a tuple (array) of 2 or 3 elements: [field, operator, value?]
{
"logic": "and",
"conditions": [
["<source table field>", "<operator>", { "type": "constant", "value": "<val>" }]
]
}
For empty / non_empty, the value can be omitted (2-element tuple):
["<source table field>", "empty"]
Constant value — for fixed conditions (e.g., "status is completed"):
["状态", "==", { "type": "constant", "value": "已完成" }]
Field reference — for dynamic per-row matching (e.g., "match current row's project"):
["项目名", "==", { "type": "field_ref", "field": "项目名" }]
Decision guide: Fixed condition (e.g., "status is completed") → constant. Dynamic condition (e.g., "match current record's project ID") → field_ref.
The value inside { "type": "constant", "value": ... } varies by field type:
| Field type | Constant value format | Example |
|---|---|---|
| Text / Phone / Email / Url | String | "已完成" |
| Number / Currency / Progress / Rating | Number | 100, 0.8 |
| DateTime / CreatedTime / ModifiedTime | Duration tuple | ["ExactDate", "2025-01-01"], ["Today"], ["Yesterday"], ["Tomorrow"] |
| SingleSelect / MultiSelect | Option ID or ID array | "opt_xxx", ["opt_xxx", "opt_yyy"] |
| Link (SingleLink / DuplexLink) | Record ID or ID array | "rec_xxx", ["rec_xxx", "rec_yyy"] |
| User | User ID or ID array | "123", ["123", "456"] |
| Checkbox | Boolean | true, false |
| Attachment / Location | Only empty / non_empty |
value must be null or omitted |
| AutoNumber | Not supported for constant comparison | Use dynamic field_ref instead |
| Formula / Lookup (exact type) | Follow the underlying type rules | — |
| Formula / Lookup (fuzzy type) | String | "some text" |
DateTime notes:
ExactDate, Today, Yesterday, Tomorrow are supported as duration formats["ExactDate", "2025-01-01"] means the exact moment 2025-01-01 00:00:00, NOT the entire dayWhen using { "type": "field_ref", "field": "..." }, values from both sides are first converted to sets at runtime, then compared using set operations:
==: Sets are exactly equal (strict matching)intersects: Sets have a non-empty intersection (most commonly used)Conversion rules by field type:
| Field type | Converted to |
|---|---|
| Text / Phone / Email / Url | Single-element string set |
| Number / Currency / AutoNumber / DateTime | Single-element number set |
| SingleSelect / MultiSelect | Set of option name strings |
| User | Set of user name strings |
| Link (SingleLink / DuplexLink) | Set of linked records' primary field string representations |
| Formula / Lookup | The computed value set |
Examples:
["name1", "name2"] intersects text "name1" → true; == text "name1" → false (sets not equal)["name1"] == text "name1" → true (single-element sets are equal)| Operator | Meaning | Applicable types |
|---|---|---|
== |
Equal (exact match) | All types |
!= |
Not equal | All types |
> |
Greater than | Number, DateTime |
>= |
Greater than or equal | Number, DateTime |
< |
Less than | Number, DateTime |
<= |
Less than or equal | Number, DateTime |
intersects |
Has intersection (non-empty overlap) | All types (most commonly used for dynamic field_ref) |
disjoint |
No intersection | All types |
empty |
Field is empty | All types (value must be null or omitted) |
non_empty |
Field is not empty | All types (value must be null or omitted) |
{ and: [{ or: [...] }] }) is not supported| Aggregate | Common user phrasing | Select field should be | Result type |
|---|---|---|---|
sum |
"total" / "sum" / "cumulative amount" | Numeric field (e.g., amount) | Number |
average |
"average" / "mean" | Numeric field | Number |
max |
"maximum" / "latest" / "most recent" | Numeric / DateTime field | Same as source |
min |
"minimum" / "earliest" | Numeric / DateTime field | Same as source |
counta |
"count" / "how many" / "total number" | Any field | Number |
unique_counta |
"count distinct" / "how many different" | Field to deduplicate | Number |
unique |
"list distinct" / "which ones" / "show different" | Field to display | List |
raw_value |
"list all" / "show all values" (default) | Field to display | List |
Common confusion: unique returns a deduplicated list, unique_counta returns a count. "Which categories are involved" → unique; "How many categories" → unique_counta.
Important:
sum not Sum, average not Averagecounta, NOT count — this is the most common enum mistakewhere field is required with at least one condition. Whenever the current table and source table have row-level correspondence, the condition should express that relationship.from requires changing select: Updating the source table without updating the select field will error.Step 1: Analyze the filtering semantics in the user's request
"Count artworks per exhibition" → filter: belongs to exhibition = current exhibition
"Sum completed order amounts" → filter: status = completed AND project = current project
Step 2: Find the matching field pair
├─ Tables have a Link relationship?
│ ├─ Link is in source table → source.linkField matches current.primaryField
│ └─ Link is in current table → source.primaryField matches current.linkField
├─ Tables share same-meaning text/number field? → source.field matches current.field
└─ Also need constant filtering? → AND combination
How to handle multiple matching records?
├─ Show all values as-is → raw_value (default)
├─ Show deduplicated list → unique
├─ Sum → sum
├─ Average → average
├─ Maximum / minimum → max / min
├─ Count records → counta
└─ Count distinct → unique_counta
Patterns are categorized by filter matching method. Aggregate choice is independent — see Section 4.
Scenario: "Count artworks per exhibition", "Sum order amounts per project"
When the source table has a Link pointing to the current table:
Exhibition table: ExhibitionName (primaryField) ← current table
Artwork table: ArtworkName (primaryField), ← source table (Link is here)
Exhibition (Link → Exhibition table)
{
"type": "lookup",
"name": "Artwork Count",
"from": "Artwork table",
"select": "ArtworkName",
"aggregate": "counta",
"where": {
"logic": "and",
"conditions": [
["Exhibition", "intersects", { "type": "field_ref", "field": "ExhibitionName" }]
]
}
}
Scenario: "Show supplier's contact person", "Display warehouse manager"
When the current table has a Link pointing to the source table:
Supplier table: SupplierName (primaryField), Contact (Text) ← source table
Inventory table: ProductName (primaryField), ← current table (Link is here)
Supplier (Link → Supplier table)
{
"type": "lookup",
"name": "Supplier Contact",
"from": "Supplier table",
"select": "Contact",
"where": {
"logic": "and",
"conditions": [
["SupplierName", "intersects", { "type": "field_ref", "field": "Supplier" }]
]
}
}
Scenario: "Sum order amounts per project" (tables share a "ProjectName" field but no Link)
Project table: ProjectName (primaryField) ← current table
Order table: OrderID (primaryField), ProjectName (Text), ← source table
Amount (Number)
{
"type": "lookup",
"name": "Order Total",
"from": "Order table",
"select": "Amount",
"aggregate": "sum",
"where": {
"logic": "and",
"conditions": [
["ProjectName", "==", { "type": "field_ref", "field": "ProjectName" }]
]
}
}
Scenario: "Only count completed orders", "Only sum approved budgets"
Combine row-level matching with fixed-value filtering using logic: "and":
{
"type": "lookup",
"name": "Completed Order Amount",
"from": "Order table",
"select": "Amount",
"aggregate": "sum",
"where": {
"logic": "and",
"conditions": [
["Manager", "==", { "type": "field_ref", "field": "EmployeeName" }],
["Status", "==", { "type": "constant", "value": "Completed" }]
]
}
}
Scenario: "Look up orders created after 2025-01-01", "Sum today's sales"
{
"type": "lookup",
"name": "Recent Orders",
"from": "Order table",
"select": "Amount",
"aggregate": "sum",
"where": {
"logic": "and",
"conditions": [
["ProjectName", "==", { "type": "field_ref", "field": "ProjectName" }],
["CreatedDate", ">=", { "type": "constant", "value": ["ExactDate", "2025-01-01"] }]
]
}
}
// Wrong: no where, every row pulls all records
{ "type": "lookup", "name": "Artwork Count", "from": "Artwork table", "select": "ArtworkName", "aggregate": "counta" }
// Correct: where with Link relationship
{ "type": "lookup", "name": "Artwork Count", "from": "Artwork table", "select": "ArtworkName", "aggregate": "counta",
"where": { "logic": "and", "conditions": [
["Exhibition", "intersects", { "type": "field_ref", "field": "ExhibitionName" }]
]}}
// Wrong: using constant for a dynamic join
["ProjectName", "==", { "type": "constant", "value": "ProjectName" }]
// Correct: use field_ref for dynamic per-row matching
["ProjectName", "==", { "type": "field_ref", "field": "ProjectName" }]
count instead of counta// Wrong
{ "aggregate": "count" }
// Correct
{ "aggregate": "counta" }
// Wrong
{ "aggregate": "SUM" }
{ "aggregate": "Sum" }
// Correct — snake_case lowercase
{ "aggregate": "sum" }
{ "aggregate": "average" }
// Wrong: nesting not supported
{ "logic": "and", "conditions": [
{ "logic": "or", "conditions": [...] }
]}
// Correct: only one level
{ "logic": "and", "conditions": [cond1, cond2, cond3] }
The user says "aggregate order amounts" — use Lookup, not Link. Link establishes relationships; Lookup retrieves and aggregates data.
// Wrong: object format
{ "fieldRef": "Status", "operator": "is", "value": { "type": "constant", "value": "Done" } }
// Correct: tuple format [field, operator, value?]
["Status", "==", { "type": "constant", "value": "Done" }]
type field// Wrong: no type field
{ "name": "Total", "from": "Orders", "select": "Amount", "aggregate": "sum", "where": { ... } }
// Correct: must include type
{ "type": "lookup", "name": "Total", "from": "Orders", "select": "Amount", "aggregate": "sum", "where": { ... } }
type must be "lookup" — this field is required in the request bodywhere is required with at least one condition — always specify a filter[field, operator, value?] — NOT object formatsum, counta, unique_counta (NOT count)==, !=, >, >=, <, <=, intersects, disjoint, empty, non_empty+table-get output["ExactDate", "2025-01-01"], ["Today"], ["Yesterday"], ["Tomorrow"]