en un clic
object-builder
// Full 3-step workflow for creating workspace objects (SQL → filesystem → verify), CRM patterns for common object types, kanban boards, and the post-mutation checklist.
// Full 3-step workflow for creating workspace objects (SQL → filesystem → verify), CRM patterns for common object types, kanban boards, and the post-mutation checklist.
Build data-driven DenchClaw apps with full CRUD access to workspace objects (.object.yaml tables), DuckDB queries and mutations, data dashboards with Chart.js and D3.js, and interactive tools.
Build and manage DenchClaw apps — self-contained web applications that run inside the workspace with access to DuckDB data, workspace objects, AI chat, and the full DenchClaw platform API.
DuckDB schema initialization, field types reference, auto-generated PIVOT views, and SQL CRUD operations for workspace objects, fields, and entries.
Manage DuckDB CRM data, aggressive relation-linked fields, and synced markdown documents in the workspace. Use when creating or updating objects, fields, entries, foreign-table links, row notes, or entry-linked edit logs.
.object.yaml format and template, view type settings (kanban, calendar, timeline, gallery, list), saved views with filter operators, and date format rules.
Build AI-powered DenchClaw apps that interact with the OpenClaw agent — create chat sessions, send and receive messages with streaming, expose app tools for agent invocation, and access agent memory.
| name | object-builder |
| description | Full 3-step workflow for creating workspace objects (SQL → filesystem → verify), CRM patterns for common object types, kanban boards, and the post-mutation checklist. |
| metadata | {"openclaw":{"inject":true,"always":true,"emoji":"🏗️"}} |
This skill covers creating and modifying workspace objects end-to-end. For DuckDB schema and SQL reference, see duckdb-operations (crm/duckdb-operations/SKILL.md). For workspace fundamentals, see the parent crm skill (crm/SKILL.md).
Object icons live ONLY in <objectDir>/.object.yaml under the icon: key.
Never include an icon column in any DuckDB SQL — the column has been retired.
To change an existing object's icon, edit its .object.yaml directly OR call
PATCH /api/workspace/objects/<name>/icon with { "icon": "lucide-name" }.
The web UI also exposes an icon picker in the object header.
EVERY object creation MUST complete ALL THREE steps below. Never stop after the SQL.
Step 1 — SQL: Create object + fields + view (single exec call):
BEGIN TRANSACTION;
-- 1a. Create object (icon is NOT a DB column — it lives in .object.yaml only)
INSERT INTO objects (name, description, default_view)
VALUES ('lead', 'Sales leads tracking', 'table')
ON CONFLICT (name) DO NOTHING;
-- 1b. Create all fields
INSERT INTO fields (object_id, name, type, required, sort_order) VALUES
((SELECT id FROM objects WHERE name = 'lead'), 'Full Name', 'text', true, 0),
((SELECT id FROM objects WHERE name = 'lead'), 'Email Address', 'email', true, 1),
((SELECT id FROM objects WHERE name = 'lead'), 'Phone Number', 'phone', false, 2),
((SELECT id FROM objects WHERE name = 'lead'), 'Score', 'number', false, 4),
((SELECT id FROM objects WHERE name = 'lead'), 'Notes', 'richtext', false, 7)
ON CONFLICT (object_id, name) DO NOTHING;
INSERT INTO fields (object_id, name, type, enum_values, enum_colors, sort_order) VALUES
((SELECT id FROM objects WHERE name = 'lead'), 'Status', 'enum',
'["New","Contacted","Qualified","Converted"]'::JSON,
'["#94a3b8","#3b82f6","#f59e0b","#22c55e"]'::JSON, 3),
((SELECT id FROM objects WHERE name = 'lead'), 'Source', 'enum',
'["Website","Referral","Cold Call","Social"]'::JSON, NULL, 5)
ON CONFLICT (object_id, name) DO NOTHING;
-- 1b-2. Link to company object if it exists (proactive relation)
INSERT INTO fields (object_id, name, type, related_object_id, relationship_type, sort_order)
SELECT
(SELECT id FROM objects WHERE name = 'lead'),
'Company',
'relation',
(SELECT id FROM objects WHERE name = 'company'),
'many_to_one',
6
WHERE EXISTS (SELECT 1 FROM objects WHERE name = 'company')
ON CONFLICT (object_id, name) DO NOTHING;
-- 1c. MANDATORY: auto-generate PIVOT view (list all non-action fields in IN clause)
CREATE OR REPLACE VIEW v_lead AS
PIVOT (
SELECT e.id as entry_id, e.created_at, e.updated_at,
f.name as field_name, ef.value
FROM entries e
JOIN entry_fields ef ON ef.entry_id = e.id
JOIN fields f ON f.id = ef.field_id
WHERE e.object_id = (SELECT id FROM objects WHERE name = 'lead')
AND f.type != 'action'
) ON field_name IN ('Full Name', 'Email Address', 'Phone Number', 'Status', 'Score', 'Source', 'Company', 'Notes') USING first(value);
COMMIT;
Step 2 — Filesystem: Create object directory + .object.yaml (exec call):
mkdir -p {{WORKSPACE_PATH}}/lead
# Query actual values from DuckDB (do NOT use placeholder strings)
OBJ_ID=$(duckdb {{WORKSPACE_PATH}}/workspace.duckdb -noheader -list "SELECT id FROM objects WHERE name = 'lead'")
ENTRY_COUNT=$(duckdb {{WORKSPACE_PATH}}/workspace.duckdb -noheader -list "SELECT COUNT(*) FROM entries WHERE object_id = '$OBJ_ID'")
# Write .object.yaml using the actual queried values (note: no 'YAML' — we need variable expansion)
cat > {{WORKSPACE_PATH}}/lead/.object.yaml << EOF
id: "$OBJ_ID"
name: "lead"
description: "Sales leads tracking"
icon: "user-plus"
default_view: "table"
entry_count: $ENTRY_COUNT
fields:
- name: "Full Name"
type: text
required: true
- name: "Email Address"
type: email
required: true
- name: "Phone Number"
type: phone
- name: "Status"
type: enum
values: ["New", "Contacted", "Qualified", "Converted"]
- name: "Score"
type: number
- name: "Source"
type: enum
values: ["Website", "Referral", "Cold Call", "Social"]
- name: "Company"
type: relation
related_object: company
relationship_type: many_to_one
- name: "Notes"
type: richtext
EOF
Step 3 — Verify: Confirm both the view and filesystem exist:
# Verify view works
duckdb {{WORKSPACE_PATH}}/workspace.duckdb "SELECT COUNT(*) FROM v_lead"
# Verify .object.yaml exists
cat {{WORKSPACE_PATH}}/lead/.object.yaml
When creating task/board objects, use default_view = 'kanban' and auto-create Status + Assigned To fields. Set view_settings.kanbanField to the enum field that defines columns. Remember: ALL THREE STEPS are required.
Step 1 — SQL:
BEGIN TRANSACTION;
INSERT INTO objects (name, description, default_view)
VALUES ('task', 'Task tracking board', 'kanban')
ON CONFLICT (name) DO NOTHING;
-- Auto-create Status field with kanban-appropriate values
INSERT INTO fields (object_id, name, type, enum_values, enum_colors, sort_order)
VALUES ((SELECT id FROM objects WHERE name = 'task'), 'Status', 'enum',
'["In Queue","In Progress","Done"]'::JSON,
'["#94a3b8","#3b82f6","#22c55e"]'::JSON, 0)
ON CONFLICT (object_id, name) DO NOTHING;
-- Auto-create Assigned To field (user type)
INSERT INTO fields (object_id, name, type, sort_order)
VALUES ((SELECT id FROM objects WHERE name = 'task'), 'Assigned To', 'user', 1)
ON CONFLICT (object_id, name) DO NOTHING;
-- Auto-create default statuses
INSERT INTO statuses (object_id, name, color, sort_order, is_default) VALUES
((SELECT id FROM objects WHERE name = 'task'), 'In Queue', '#94a3b8', 0, true),
((SELECT id FROM objects WHERE name = 'task'), 'In Progress', '#3b82f6', 1, false),
((SELECT id FROM objects WHERE name = 'task'), 'Done', '#22c55e', 2, false)
ON CONFLICT (object_id, name) DO NOTHING;
CREATE OR REPLACE VIEW v_task AS
PIVOT (
SELECT e.id as entry_id, e.created_at, e.updated_at,
f.name as field_name, ef.value
FROM entries e
JOIN entry_fields ef ON ef.entry_id = e.id
JOIN fields f ON f.id = ef.field_id
WHERE e.object_id = (SELECT id FROM objects WHERE name = 'task')
AND f.type != 'action'
) ON field_name IN ('Title', 'Description', 'Status', 'Priority', 'Due Date', 'Assigned To', 'Notes') USING first(value);
COMMIT;
Step 2 — Filesystem (MANDATORY):
mkdir -p {{WORKSPACE_PATH}}/task
OBJ_ID=$(duckdb {{WORKSPACE_PATH}}/workspace.duckdb -noheader -list "SELECT id FROM objects WHERE name = 'task'")
cat > {{WORKSPACE_PATH}}/task/.object.yaml << EOF
id: "$OBJ_ID"
name: "task"
description: "Task tracking board"
icon: "check-square"
default_view: "kanban"
entry_count: 0
view_settings:
kanbanField: "Status"
fields:
- name: "Status"
type: enum
values: ["In Queue", "In Progress", "Done"]
- name: "Assigned To"
type: user
EOF
Step 3 — Verify: duckdb {{WORKSPACE_PATH}}/workspace.duckdb "SELECT COUNT(*) FROM v_task" and cat {{WORKSPACE_PATH}}/task/.object.yaml.
When creating multiple objects or adding fields to an existing object, ALWAYS create relation fields to link them — even if the user did not explicitly ask for it. Real-world data is interconnected. If two objects are obviously related, link them. The user expects this; not linking them is a missed opportunity that forces manual work later.
Before you create any new field on an object:
SELECT name FROM objects ORDER BY namerelation field via SQL instead of a scalar field via API/text defaults.object.yaml so the linked field is reflected everywhereAggressive defaults:
relation field.relation field.user, not text.Company Name, Client Name, Project Name, Deal Name, or Owner Name when the real object already exists, unless the user explicitly asks for a copied text snapshot.many_to_one; switch to many_to_many only when the field is clearly plural or multi-select.General rule: If you're creating object B, or adding field F to object B, and object A already exists (or is being created alongside), ask yourself: "Would an entry in B logically belong to, reference, select, or connect to an entry in A?" If yes, add a relation field.
INSERT INTO fields (object_id, name, type, related_object_id, relationship_type, sort_order)
VALUES (
(SELECT id FROM objects WHERE name = 'people'),
'Company',
'relation',
(SELECT id FROM objects WHERE name = 'company'),
'many_to_one',
3
) ON CONFLICT (object_id, name) DO NOTHING;
Use many_to_one when each entry links to exactly one entry in the other object (most common). Use many_to_many when an entry can link to multiple entries (e.g., project → team members).
Relation fields must be created via SQL — the API does not support the relation type.
Company Name as text on lead when company already existsCompany as relation -> companyProject as text on task when project already existsProject as relation -> projectOwner Name as text when the value should be a team member selectorOwner / Assigned To as userdefault_view = 'kanban' — auto-creates Status and Assigned To fieldsYou MUST complete ALL steps below after ANY schema mutation (create/update/delete object, field, or entry). Do NOT skip any step. Do NOT consider the operation complete until all steps are done.
CREATE OR REPLACE VIEW v_{object_name} — regenerate the PIVOT viewmkdir -p {{WORKSPACE_PATH}}/{object_name}/ — create the object directory{{WORKSPACE_PATH}}/{object_name}/.object.yaml — metadata projection with id, name, description, icon (yaml is the only place icons live), default_view, entry_count, and full field listparent_document_id, place directory inside the parent document's directoryWORKSPACE.md if it existsentry_count in the corresponding .object.yamlSELECT * FROM v_{object} LIMIT 5DROP VIEW IF EXISTS v_{object_name} — remove the viewrm -rf {{WORKSPACE_PATH}}/{object_name}/ — remove the directory (unless it contains nested documents that need relocating)WORKSPACE.md.md file to the correct path in {{WORKSPACE_PATH}}/**INSERT INTO documents — ensure metadata row exists with correct file_path, parent_id, or parent_object_idmkdir -p {{WORKSPACE_PATH}}/{object_name}/.actions/ — create the actions directoryscriptPath in the action config (e.g. .actions/send-email.js)AND f.type != 'action').object.yaml with the action field including action_configls {{WORKSPACE_PATH}}/{object_name}/.actions/See the actions child skill (crm/actions/SKILL.md) for the complete end-to-end action creation walkthrough.
These steps ensure the filesystem always mirrors DuckDB. The sidebar depends on .object.yaml files — if they are missing, objects will not appear.
The .object.yaml file is missing. Regenerate it:
OBJ_ID=$(duckdb {{WORKSPACE_PATH}}/workspace.duckdb -noheader -list "SELECT id FROM objects WHERE name = 'lead'")
mkdir -p {{WORKSPACE_PATH}}/lead
# Then write .object.yaml with the correct id, name, fields, and entry_count
The IN (...) field list is out of date. Regenerate the view by querying current fields:
duckdb {{WORKSPACE_PATH}}/workspace.duckdb -json "SELECT name FROM fields WHERE object_id = (SELECT id FROM objects WHERE name = 'lead') AND type != 'action' ORDER BY sort_order"
# Use the output to rebuild the IN clause in the PIVOT view
.object.yaml is out of sync with DuckDBQuery the current state and rewrite the file:
OBJ_ID=$(duckdb {{WORKSPACE_PATH}}/workspace.duckdb -noheader -list "SELECT id FROM objects WHERE name = 'lead'")
ENTRY_COUNT=$(duckdb {{WORKSPACE_PATH}}/workspace.duckdb -noheader -list "SELECT COUNT(*) FROM entries WHERE object_id = '$OBJ_ID'")
duckdb {{WORKSPACE_PATH}}/workspace.duckdb -json "SELECT name, type, required, enum_values, default_value FROM fields WHERE object_id = '$OBJ_ID' ORDER BY sort_order"
# Use these values to rebuild .object.yaml
All three must be identical: the DuckDB objects.name, the filesystem directory name, and .object.yaml name. If they diverge, rename them to match. See the "Renaming / Moving Objects" section in the parent CRM skill.