# Base Lookup Field Configuration Guide ## Mandatory Read Acknowledgement 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. ## Default strategy **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. ## Usage When creating a lookup field, the Agent should: 1. Get all table names: `lark-cli base +table-list --base-token ` — returns `items[].table_name` 2. Get table structure: `lark-cli base +table-get --base-token --table-id ` — returns `fields[]` 3. If the lookup references other tables, also get those tables' structures 4. Determine the four elements: from (source table), select (source field), where (filter), aggregate (aggregation) 5. Construct the Lookup field JSON and submit it to create or update the field **Key constraints**: - Table names and field names must **exactly match** those returned by `+table-list` / `+table-get` - The `from` table must be in the same Base --- ## Section 1: Core Concepts — Four-Element Model A 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.** --- ## Section 2: Lookup vs Link vs Formula 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 ``` --- ## Section 3: Filter Condition Rules **You must provide a `where` with at least one condition.** Improper conditions cause every row to pull all records from the source table. ### The Iron Rule: field belongs to 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 ``` ### How to find the matching field pair **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. ### Where condition structure Each condition is a **tuple** (array) of 2 or 3 elements: `[field, operator, value?]` ```json { "logic": "and", "conditions": [ ["", "", { "type": "constant", "value": "" }] ] } ``` For `empty` / `non_empty`, the value can be omitted (2-element tuple): ```json ["", "empty"] ``` ### Two value formats **Constant value** — for fixed conditions (e.g., "status is completed"): ```json ["状态", "==", { "type": "constant", "value": "已完成" }] ``` **Field reference** — for dynamic per-row matching (e.g., "match current row's project"): ```json ["项目名", "==", { "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`. ### Constant value format by field type The `value` inside `{ "type": "constant", "value": ... }` varies by field type: | Field type | Constant value format | Example | |-----------|----------------------|---------| | `text` | String | `"已完成"` | | `number` | Number | `100`, `0.8` | | `datetime` / `created_at` / `updated_at` | String | `"ExactDate(2025-01-01)"`, `"ExactDate(2025-01-01 09:30)"`, `"Today"`, `"Yesterday"`, `"Tomorrow"` | | `select` (`multiple=false/true`) | Option name array | `["Todo"]`, `["Todo", "Done"]` | | `link` | Record reference array | `[{ "id": "rec_xxx" }]`, `[{ "id": "rec_xxx" }, { "id": "rec_yyy" }]` | | `user` / `created_by` / `updated_by` | User reference array | `[{ "id": "ou_xxx" }]`, `[{ "id": "ou_xxx" }, { "id": "ou_yyy" }]` | | `checkbox` | Boolean | `true`, `false` | | `attachment` / `location` | Only `empty` / `non_empty` | value must be `null` or omitted | | `auto_number` | 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**: - Supported datetime constant values are `ExactDate(...)`, `Today`, `Yesterday`, `Tomorrow` - Date-only fields use `ExactDate(YYYY-MM-DD)` - Fields that include time use `ExactDate(YYYY-MM-DD HH:mm)` - For complex or relative date filtering, consider using a Formula field instead ### Dynamic field reference — set comparison semantics When 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` | Single-element string set | | `number` / `auto_number` / `datetime` | Single-element number set | | `select` (`multiple=false/true`) | Set of option name strings | | `user` / `created_by` / `updated_by` | Set of user name strings | | `link` | Set of linked records' primary field string representations | | `formula` / `lookup` | The computed value set | **Examples**: - User field `["name1", "name2"]` **intersects** text `"name1"` → true; **==** text `"name1"` → false (sets not equal) - User field `["name1"]` **==** text `"name1"` → true (single-element sets are equal) - Link field referencing records → converted to primary field strings, then compared ### Supported operators | Operator | Meaning | Applicable field 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) | ### Constraints - **Only one level of and/or** — nesting (e.g., `{ and: [{ or: [...] }] }`) is not supported - **At least one condition** — empty conditions array will error --- ## Section 4: Aggregate Rules | Aggregate | Common user phrasing | Select field should be | Result type | |-----------|---------------------|----------------------|-------------| | `sum` | "total" / "sum" / "cumulative amount" | `number` field (e.g., amount) | Number | | `average` | "average" / "mean" | `number` field | Number | | `max` | "maximum" / "latest" / "most recent" | `number` / `datetime` field | Same as source | | `min` | "minimum" / "earliest" | `number` / `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**: - Enum values are **snake_case lowercase**: `sum` not `Sum`, `average` not `Average` - **Count is `counta`, NOT `count`** — this is the most common enum mistake --- ## Section 5: Hard Constraints 1. **Always write a filter**: The `where` 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. 2. **Lookup fields are read-only**: Cell values cannot be manually set. 3. **Create Lookup after all dependent fields exist**: The source table and referenced fields must exist before creating the Lookup field. 4. **Source table must be in the same Base**: Cross-Base lookups are not supported. 5. **Changing `from` requires changing `select`**: Updating the source table without updating the select field will error. --- ## Section 6: Decision Trees ### How to build the filter ``` 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 ``` ### Which aggregate? ``` 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 ``` --- ## Section 7: Common Configuration Patterns > Patterns are categorized by **filter matching method**. Aggregate choice is independent — see Section 4. ### Pattern 1: Aggregate from a linked table (Link is in the source table) **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) ``` ```json { "type": "lookup", "name": "Artwork Count", "from": "Artwork table", "select": "ArtworkName", "aggregate": "counta", "where": { "logic": "and", "conditions": [ ["Exhibition", "intersects", { "type": "field_ref", "field": "ExhibitionName" }] ] } } ``` ### Pattern 2: Reference a linked record's field (Link is in the current table) **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) ``` ```json { "type": "lookup", "name": "Supplier Contact", "from": "Supplier table", "select": "Contact", "where": { "logic": "and", "conditions": [ ["SupplierName", "intersects", { "type": "field_ref", "field": "Supplier" }] ] } } ``` ### Pattern 3: Match by same-meaning field (no Link) **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) ``` ```json { "type": "lookup", "name": "Order Total", "from": "Order table", "select": "Amount", "aggregate": "sum", "where": { "logic": "and", "conditions": [ ["ProjectName", "==", { "type": "field_ref", "field": "ProjectName" }] ] } } ``` ### Pattern 4: Dynamic matching + constant filtering **Scenario**: "Only count completed orders", "Only sum approved budgets" Combine row-level matching with fixed-value filtering using `logic: "and"`: ```json { "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" }] ] } } ``` ### Pattern 5: Date filtering with constant value **Scenario**: "Look up orders created after 2025-01-01", "Sum today's sales" ```json { "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)" }] ] } } ``` --- ## Section 8: Anti-Pattern Collection ### Mistake 1: Omitting where (most common) ```json // 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" }] ]}} ``` ### Mistake 2: Wrong value type — confusing constant vs field_ref ```json // 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" }] ``` ### Mistake 3: Using `count` instead of `counta` ```json // Wrong { "aggregate": "count" } // Correct { "aggregate": "counta" } ``` ### Mistake 4: Wrong case for aggregate values ```json // Wrong { "aggregate": "SUM" } { "aggregate": "Sum" } // Correct — snake_case lowercase { "aggregate": "sum" } { "aggregate": "average" } ``` ### Mistake 5: Nested where conditions ```json // Wrong: nesting not supported { "logic": "and", "conditions": [ { "logic": "or", "conditions": [...] } ]} // Correct: only one level { "logic": "and", "conditions": [cond1, cond2, cond3] } ``` ### Mistake 6: Confusing Lookup with Link The user says "aggregate order amounts" — use Lookup, not Link. Link establishes relationships; Lookup retrieves and aggregates data. ### Mistake 7: Using object format instead of tuple for conditions ```json // Wrong: object format { "fieldRef": "Status", "operator": "is", "value": { "type": "constant", "value": "Done" } } // Correct: tuple format [field, operator, value?] ["Status", "==", { "type": "constant", "value": "Done" }] ``` ### Mistake 8: Missing `type` field ```json // 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": { ... } } ``` --- ## Section 9: Constraint Summary - `type` must be `"lookup"` — this field is required in the request body - `where` is required with at least one condition — always specify a filter - Conditions use **tuple format**: `[field, operator, value?]` — NOT object format - Lookup fields are read-only — values cannot be manually set - Source table and referenced fields must exist before creating the Lookup - Condition field (first element of tuple) must reference a field in the source table, not the current table - Where supports only one level of and/or — no nesting - Aggregate values are snake_case lowercase: `sum`, `counta`, `unique_counta` (NOT `count`) - Operators: `==`, `!=`, `>`, `>=`, `<`, `<=`, `intersects`, `disjoint`, `empty`, `non_empty` - Table and field names must exactly match `+table-get` output - `datetime` constant values use string format: `ExactDate(YYYY-MM-DD)` / `ExactDate(YYYY-MM-DD HH:mm)` / `Today` / `Yesterday` / `Tomorrow` - `select` constant values use option names; - `link` / `user` constant values use `{id}` object arrays