flowbase-cli 0.1.12__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1364 @@
1
+ """CLI endpoint catalog, contract-tested against the server OpenAPI schema."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class Endpoint:
9
+ key: str
10
+ operation_id: str
11
+ method: str
12
+ path: str
13
+ scope: str
14
+ summary: str
15
+ example: str
16
+ requires_confirmation: bool = False
17
+ environments: tuple[str, ...] = ("production", "test-a", "test-b")
18
+ preflight_operation_id: str | None = None
19
+ audit_event: str | None = None
20
+ # Representative request JSON for the CLI --json/--json-file body, if any.
21
+ request_body: str | None = None
22
+
23
+
24
+ ENDPOINTS = (
25
+ Endpoint("pages.evaluate", "flowbase_pages_evaluate", "POST", "/pages/{page_id}/evaluate", "metadata:read", "Evaluate draft page rule effects on sample field-key values; no writes or visual rendering.", "flowbase pages evaluate pag_xxx --json-file values.json", request_body='{"values":{"done":true}}'),
26
+ Endpoint("dashboards.run", "flowbase_dashboards_run", "POST", "/dashboards/{dashboard_id}/components/{component_id}/run", "metadata:read", "Run a saved draft component; also requires records:read. Inspect ok/output/error, not visual rendering.", "flowbase dashboards run dsh_xxx dcm_xxx --json-file filters.json", request_body='{"filterValues":{}}'),
27
+ Endpoint("organization.employees", "flowbase_organization_employees", "GET", "/organization/employees", "metadata:read", "Paginated employee IDs/names; follow pages with --page, optionally --query.", "flowbase organization employees --page 1"),
28
+ Endpoint("organization.departments", "flowbase_organization_departments", "GET", "/organization/departments", "metadata:read", "Department IDs/names/parentId.", "flowbase organization departments"),
29
+ Endpoint("apps.list", "flowbase_apps_list", "GET", "/apps", "metadata:read", "List apps.", "flowbase apps list"),
30
+ Endpoint("apps.get", "flowbase_apps_get", "GET", "/apps/{app_id}", "metadata:read", "Get app metadata with groups and objects.", "flowbase apps get app_xxx"),
31
+ Endpoint("apps.create", "flowbase_apps_create", "POST", "/apps", "metadata:write", "Create an app draft.", "flowbase apps create --json-file app.json",
32
+ request_body='{"label":"CRM","key":"crm","icon":"IconApps","color":"#2563EB"}'),
33
+ Endpoint("apps.update", "flowbase_apps_update", "PUT", "/apps/{app_id}", "metadata:write", "Update app appearance; label, icon and color are required together (read apps.get first).", "flowbase apps update app_xxx --label CRM --icon IconApps --color '#2563EB'",
34
+ request_body='{"label":"CRM","icon":"IconApps","color":"#2563EB"}'),
35
+ Endpoint("apps.set-enabled", "flowbase_apps_set_enabled", "POST", "/apps/{app_id}/enabled", "metadata:write", "Enable or disable an app.", "flowbase apps set-enabled app_xxx --enabled --yes", True,
36
+ request_body='{"enabled":true}'),
37
+ Endpoint("apps.delete", "flowbase_apps_delete", "DELETE", "/apps/{app_id}", "metadata:write", "Delete an app (unlinks objects).", "flowbase apps delete app_xxx --yes", True),
38
+ Endpoint(
39
+ "apps.groups.create",
40
+ "flowbase_apps_groups_create",
41
+ "POST",
42
+ "/apps/{app_id}/groups",
43
+ "metadata:write",
44
+ "Create an app group draft.",
45
+ "flowbase groups create app_xxx --name Sales --icon IconFolder",
46
+ request_body='{"name":"Sales","icon":"IconFolder"}',
47
+ ),
48
+ Endpoint(
49
+ "apps.groups.reorder",
50
+ "flowbase_apps_groups_reorder",
51
+ "PUT",
52
+ "/apps/{app_id}/groups/order",
53
+ "metadata:write",
54
+ "Reorder app groups.",
55
+ "flowbase groups reorder app_xxx --json '{\"groupIds\":[\"grp_a\",\"grp_b\"]}'",
56
+ request_body='{"groupIds":["grp_a","grp_b"]}',
57
+ ),
58
+ Endpoint(
59
+ "apps.groups.update",
60
+ "flowbase_apps_groups_update",
61
+ "PUT",
62
+ "/apps/{app_id}/groups/{group_id}",
63
+ "metadata:write",
64
+ "Update an app group draft.",
65
+ "flowbase groups update app_xxx grp_xxx --name Sales",
66
+ request_body='{"name":"Sales","icon":"IconFolder"}',
67
+ ),
68
+ Endpoint(
69
+ "apps.groups.delete",
70
+ "flowbase_apps_groups_delete",
71
+ "DELETE",
72
+ "/apps/{app_id}/groups/{group_id}",
73
+ "metadata:write",
74
+ "Delete an app group (objects become ungrouped).",
75
+ "flowbase groups delete app_xxx grp_xxx --yes",
76
+ True,
77
+ ),
78
+ Endpoint(
79
+ "apps.objects.add",
80
+ "flowbase_apps_objects_add",
81
+ "POST",
82
+ "/apps/{app_id}/objects",
83
+ "metadata:write",
84
+ "Add objects to an app (association only).",
85
+ "flowbase apps add-objects app_xxx --object-ids obj_a,obj_b",
86
+ request_body='{"objectIds":["obj_a","obj_b"]}',
87
+ ),
88
+ Endpoint(
89
+ "apps.objects.reorder",
90
+ "flowbase_apps_objects_reorder",
91
+ "PUT",
92
+ "/apps/{app_id}/objects/order",
93
+ "metadata:write",
94
+ "Reorder app-linked objects.",
95
+ "flowbase apps reorder-objects app_xxx --json '{\"objectIds\":[\"obj_a\",\"obj_b\"]}'",
96
+ request_body='{"objectIds":["obj_a","obj_b"]}',
97
+ ),
98
+ Endpoint(
99
+ "apps.objects.remove",
100
+ "flowbase_apps_objects_remove",
101
+ "DELETE",
102
+ "/apps/{app_id}/objects/{object_id}",
103
+ "metadata:write",
104
+ "Remove an object association from an app.",
105
+ "flowbase apps remove-object app_xxx obj_xxx --yes",
106
+ True,
107
+ ),
108
+ Endpoint(
109
+ "apps.objects.set-group",
110
+ "flowbase_apps_objects_set_group",
111
+ "POST",
112
+ "/apps/{app_id}/objects/{object_id}/group",
113
+ "metadata:write",
114
+ "Move an app-linked object into a group or ungroup it.",
115
+ "flowbase apps set-group app_xxx obj_xxx --group-id grp_xxx",
116
+ request_body='{"groupId":"grp_xxx"}',
117
+ ),
118
+ Endpoint(
119
+ "apps.dashboards.add",
120
+ "flowbase_apps_dashboards_add",
121
+ "POST",
122
+ "/apps/{app_id}/dashboards",
123
+ "metadata:write",
124
+ "Add dashboards to an app (association only).",
125
+ "flowbase apps add-dashboards app_xxx dsh_xxx",
126
+ request_body='{"dashboardIds":["dsh_xxx"]}',
127
+ ),
128
+ Endpoint(
129
+ "apps.dashboards.remove",
130
+ "flowbase_apps_dashboards_remove",
131
+ "DELETE",
132
+ "/apps/{app_id}/dashboards/{dashboard_id}",
133
+ "metadata:write",
134
+ "Remove a dashboard association from an app.",
135
+ "flowbase apps remove-dashboard app_xxx dsh_xxx --yes",
136
+ True,
137
+ ),
138
+ Endpoint(
139
+ "apps.dashboards.set-group",
140
+ "flowbase_apps_dashboards_set_group",
141
+ "POST",
142
+ "/apps/{app_id}/dashboards/{dashboard_id}/group",
143
+ "metadata:write",
144
+ "Move an app-linked dashboard into a group or ungroup it.",
145
+ "flowbase apps set-dashboard-group app_xxx dsh_xxx --group-id grp_xxx",
146
+ request_body='{"groupId":"grp_xxx"}',
147
+ ),
148
+ Endpoint(
149
+ "apps.members.reorder",
150
+ "flowbase_apps_members_reorder",
151
+ "PUT",
152
+ "/apps/{app_id}/members/order",
153
+ "metadata:write",
154
+ "Reorder app content members (objects and dashboards mixed).",
155
+ "flowbase apps reorder-members app_xxx --json '{\"items\":[{\"type\":\"object\",\"id\":\"obj_xxx\"}]}'",
156
+ request_body='{"items":[{"type":"object","id":"obj_xxx"},{"type":"dashboard","id":"dsh_xxx"}]}',
157
+ ),
158
+ Endpoint("dashboards.list", "flowbase_dashboards_list", "GET", "/dashboards", "metadata:read", "List dashboards.", "flowbase dashboards list"),
159
+ Endpoint("dashboards.get", "flowbase_dashboards_get", "GET", "/dashboards/{dashboard_id}", "metadata:read", "Get a dashboard definition.", "flowbase dashboards get dsh_xxx"),
160
+ Endpoint("dashboards.create", "flowbase_dashboards_create", "POST", "/dashboards", "metadata:write", "Create a dashboard draft.", "flowbase dashboards create --json-file dashboard.json",
161
+ request_body='{"label":"Sales board","key":"sales_board","icon":"IconChartPie","description":"Weekly sales overview"}'),
162
+ Endpoint("dashboards.update", "flowbase_dashboards_update", "PUT", "/dashboards/{dashboard_id}", "metadata:write", "Update a dashboard draft (full snapshot).", "flowbase dashboards update dsh_xxx --json-file dashboard.json",
163
+ request_body=(
164
+ '{"label":"Sales board","description":"...","icon":"IconChartPie","baseRevision":3,'
165
+ '"filters":[{"key":"period","name":"Period","type":"date_range",'
166
+ '"bindings":[{"objectId":"obj_xxx","fieldId":"fld_xxx"}],"default":{"preset":"last_30_days"}}],'
167
+ '"components":[{"title":"New deals","layout":{"x":0,"y":0,"w":6,"h":8},'
168
+ '"code":"import flowbase\\n\\ndef run():\\n return {\\"type\\":\\"metric\\",\\"value\\":1}\\n"}]}'
169
+ )),
170
+ Endpoint("dashboards.copy", "flowbase_dashboards_copy", "POST", "/dashboards/{dashboard_id}/copy", "metadata:write", "Copy a dashboard (not added to any app).", "flowbase dashboards copy dsh_xxx"),
171
+ Endpoint("dashboards.delete", "flowbase_dashboards_delete", "POST", "/dashboards/{dashboard_id}/delete", "metadata:write", "Logically delete a dashboard (also removed from all apps).", "flowbase dashboards delete dsh_xxx --yes", True),
172
+ Endpoint("objects.list", "flowbase_objects_list", "GET", "/objects", "metadata:read", "List object metadata.", "flowbase objects list"),
173
+ Endpoint("objects.get", "flowbase_objects_get", "GET", "/objects/{object_id}", "metadata:read", "Get object and field metadata.", "flowbase objects get obj_xxx"),
174
+ Endpoint("objects.create", "flowbase_objects_create", "POST", "/objects", "metadata:write", "Create an object draft.", "flowbase objects create --json-file object.json",
175
+ request_body='{"label":"Customer","key":"customer","description":"Customer records"}'),
176
+ Endpoint("objects.update", "flowbase_objects_update", "PUT", "/objects/{object_id}", "metadata:write", "Update an object draft.", "flowbase objects update obj_xxx --label Customer --description 'Customer records'",
177
+ request_body='{"label":"Customer","description":"Customer records","icon":"IconDatabase"}'),
178
+ Endpoint("objects.delete", "flowbase_objects_delete", "DELETE", "/objects/{object_id}", "metadata:write", "Logically delete an object.", "flowbase objects delete obj_xxx --yes", True),
179
+ Endpoint("objects.restore", "flowbase_objects_restore", "POST", "/objects/{object_id}/restore", "metadata:write", "Restore an object.", "flowbase objects restore obj_xxx --yes", True),
180
+ Endpoint(
181
+ "objects.cli-edit-policy.get",
182
+ "flowbase_objects_cli_edit_policy_get",
183
+ "GET",
184
+ "/objects/{object_id}/cli-edit-policy",
185
+ "metadata:read",
186
+ "Get object CLI field edit policies.",
187
+ "flowbase objects cli-edit-policy get obj_xxx",
188
+ ),
189
+ Endpoint(
190
+ "objects.cli-edit-policy.update",
191
+ "flowbase_objects_cli_edit_policy_update",
192
+ "PUT",
193
+ "/objects/{object_id}/cli-edit-policy",
194
+ "metadata:write",
195
+ "Update object CLI field edit policies as a complete draft snapshot.",
196
+ "flowbase objects cli-edit-policy update obj_xxx --json-file cli-policy.json --yes",
197
+ requires_confirmation=True,
198
+ request_body=(
199
+ '{"baseRevision":4,"policies":{'
200
+ '"fld_confirm":"user_approval","fld_direct":"direct"}}'
201
+ ),
202
+ ),
203
+ Endpoint("fields.create", "flowbase_fields_create", "POST", "/objects/{object_id}/fields", "metadata:write", "Create a field draft.", "flowbase fields create obj_xxx --json-file field.json",
204
+ request_body=(
205
+ '{"label":"Stage","key":"stage","description":"Pipeline stage","type":"select.single","typeVersion":"v1",'
206
+ '"permission":"optional","constraints":{"options":['
207
+ '{"id":"tmp_won","label":"Won","lifecycle":{"status":"active"}},'
208
+ '{"id":"tmp_lost","label":"Lost","lifecycle":{"status":"active"}}'
209
+ ']}}'
210
+ )),
211
+ Endpoint("fields.update", "flowbase_fields_update", "PUT", "/objects/{object_id}/fields/{field_id}", "metadata:write", "Update a field draft. constraints is a full replace — start from objects.get.",
212
+ "flowbase fields update obj_xxx fld_xxx --json-file field.json",
213
+ request_body=(
214
+ '{"label":"Stage","description":"Pipeline stage","permission":"hidden",'
215
+ '"constraints":{"options":['
216
+ '{"id":"opt_won","label":"Won","lifecycle":{"status":"active"}}'
217
+ ']}}'
218
+ )),
219
+ Endpoint("fields.delete", "flowbase_fields_delete", "DELETE", "/objects/{object_id}/fields/{field_id}", "metadata:write", "Logically delete a field.", "flowbase fields delete obj_xxx fld_xxx --yes", True),
220
+ Endpoint("fields.restore", "flowbase_fields_restore", "POST", "/objects/{object_id}/fields/{field_id}/restore", "metadata:write", "Restore a field.", "flowbase fields restore obj_xxx fld_xxx --yes", True),
221
+ Endpoint("views.list", "flowbase_views_list", "GET", "/objects/{object_id}/views", "metadata:read", "List object view metadata.", "flowbase views list obj_xxx"),
222
+ Endpoint("views.create", "flowbase_views_create", "POST", "/objects/{object_id}/views", "metadata:write", "Create an object view draft.", "flowbase views create obj_xxx --json-file view.json",
223
+ request_body=(
224
+ '{"baseRevision":3,"name":"Active","enabled":true,"match":"all",'
225
+ '"filters":['
226
+ '{"fieldId":"fld_status","operator":"eq","value":"opt_xxx"},'
227
+ '{"fieldId":"fld_done","operator":"is_true"}'
228
+ '],"defaultFieldIds":["fld_a","fld_b"],"groupFieldId":null}'
229
+ )),
230
+ Endpoint("views.update", "flowbase_views_update", "PUT", "/objects/{object_id}/views/{view_id}", "metadata:write", "Update an object view draft.", "flowbase views update obj_xxx viw_xxx --json-file view.json",
231
+ request_body=(
232
+ '{"baseRevision":4,"name":"Active","enabled":true,"match":"all",'
233
+ '"filters":['
234
+ '{"fieldId":"fld_status","operator":"eq","value":"opt_xxx"},'
235
+ '{"fieldId":"fld_done","operator":"is_true"}'
236
+ '],"defaultFieldIds":["fld_a","fld_b"],"groupFieldId":null}'
237
+ )),
238
+ Endpoint("views.reorder", "flowbase_views_reorder", "PUT", "/objects/{object_id}/views/order", "metadata:write", "Reorder object views.", "flowbase views reorder obj_xxx --json '{\"baseRevision\":4,\"viewIds\":[\"viw_xxx\"]}'",
239
+ request_body='{"baseRevision":4,"viewIds":["viw_a","viw_b"]}'),
240
+ Endpoint("views.set-enabled", "flowbase_views_set_enabled", "POST", "/objects/{object_id}/views/{view_id}/enabled", "metadata:write", "Enable or disable an object view.", "flowbase views set-enabled obj_xxx viw_xxx --base-revision 4 --enabled --yes", True,
241
+ request_body='{"baseRevision":4,"enabled":true}'),
242
+ Endpoint("views.delete", "flowbase_views_delete", "DELETE", "/objects/{object_id}/views/{view_id}", "metadata:write", "Logically delete an object view.", "flowbase views delete obj_xxx viw_xxx --base-revision 4 --yes", True,
243
+ request_body='{"baseRevision":4}'),
244
+ Endpoint("workflows.list", "flowbase_workflows_list", "GET", "/objects/{object_id}/workflows", "metadata:read", "List object workflows.", "flowbase workflows list obj_xxx"),
245
+ Endpoint("workflows.get", "flowbase_workflows_get", "GET", "/objects/{object_id}/workflows/{workflow_id}", "metadata:read", "Get a workflow graph.", "flowbase workflows get obj_xxx wfl_xxx"),
246
+ Endpoint("workflows.create", "flowbase_workflows_create", "POST", "/objects/{object_id}/workflows", "metadata:write", "Create a workflow draft.", "flowbase workflows create obj_xxx --json-file workflow.json",
247
+ request_body=(
248
+ '{"baseRevision":3,"label":"Refresh related customer","enabled":true,'
249
+ '"trigger":{"event":"update","match":"all","conditions":['
250
+ '{"fieldId":"fld_tier","relationFieldId":"fld_customer",'
251
+ '"operator":"eq","value":"vip"}]},'
252
+ '"nodes":[{"id":"wfn_trigger","type":"trigger"},'
253
+ '{"id":"wfn_update","type":"updateRelated","label":"Refresh customer",'
254
+ '"relationFieldId":"fld_customer","assignments":['
255
+ '{"fieldId":"fld_priority","action":"set","valueRef":"fld_tier",'
256
+ '"valueRefRelationFieldId":"fld_customer"}]},'
257
+ '{"id":"wfn_end","type":"end"}],'
258
+ '"edges":[{"source":"wfn_trigger","target":"wfn_update"},'
259
+ '{"source":"wfn_update","target":"wfn_end"}]}'
260
+ )),
261
+ Endpoint("workflows.update", "flowbase_workflows_update", "PUT", "/objects/{object_id}/workflows/{workflow_id}", "metadata:write", "Update a workflow graph.", "flowbase workflows update obj_xxx wfl_xxx --json-file workflow.json",
262
+ request_body=(
263
+ '{"baseRevision":4,"label":"Refresh related customer","enabled":true,'
264
+ '"trigger":{"event":"update","match":"all","conditions":['
265
+ '{"fieldId":"fld_tier","relationFieldId":"fld_customer",'
266
+ '"operator":"eq","value":"vip"}]},'
267
+ '"nodes":[{"id":"wfn_trigger","type":"trigger"},'
268
+ '{"id":"wfn_update","type":"updateRelated","label":"Refresh customer",'
269
+ '"relationFieldId":"fld_customer","assignments":['
270
+ '{"fieldId":"fld_priority","action":"set","valueRef":"fld_tier",'
271
+ '"valueRefRelationFieldId":"fld_customer"}]},'
272
+ '{"id":"wfn_end","type":"end"}],'
273
+ '"edges":[{"source":"wfn_trigger","target":"wfn_update"},'
274
+ '{"source":"wfn_update","target":"wfn_end"}]}'
275
+ )),
276
+ Endpoint("workflows.reorder", "flowbase_workflows_reorder", "PUT", "/objects/{object_id}/workflows/order", "metadata:write", "Reorder object workflows.", "flowbase workflows reorder obj_xxx --json '{\"baseRevision\":4,\"workflowIds\":[\"wfl_a\",\"wfl_b\"]}'",
277
+ request_body='{"baseRevision":4,"workflowIds":["wfl_a","wfl_b"]}'),
278
+ Endpoint("workflows.set-enabled", "flowbase_workflows_set_enabled", "POST", "/objects/{object_id}/workflows/{workflow_id}/enabled", "metadata:write", "Enable or disable a workflow.", "flowbase workflows set-enabled obj_xxx wfl_xxx --base-revision 4 --enabled --yes", True,
279
+ request_body='{"baseRevision":4,"enabled":true}'),
280
+ Endpoint("workflows.delete", "flowbase_workflows_delete", "POST", "/objects/{object_id}/workflows/{workflow_id}/delete", "metadata:write", "Logically delete a workflow.", "flowbase workflows delete obj_xxx wfl_xxx --base-revision 4 --yes", True,
281
+ request_body='{"baseRevision":4}'),
282
+ Endpoint("workflows.executions", "flowbase_workflows_executions", "GET", "/objects/{object_id}/workflows/{workflow_id}/executions", "metadata:read", "List workflow execution records.", "flowbase workflows executions obj_xxx wfl_xxx --result failure"),
283
+ Endpoint("approvals.list", "flowbase_approvals_list", "GET", "/objects/{object_id}/approvals", "metadata:read", "List object approval flows.", "flowbase approvals list obj_xxx"),
284
+ Endpoint("approvals.get", "flowbase_approvals_get", "GET", "/objects/{object_id}/approvals/{approval_id}", "metadata:read", "Get an approval flow graph.", "flowbase approvals get obj_xxx apr_xxx"),
285
+ Endpoint("approvals.create", "flowbase_approvals_create", "POST", "/objects/{object_id}/approvals", "metadata:write", "Create an approval flow draft.", "flowbase approvals create obj_xxx --json-file approval.json",
286
+ request_body=(
287
+ '{"baseRevision":3,"label":"Contract approval","enabled":true,'
288
+ '"trigger":{"manual":true,"create":false,"update":false,"match":"all","conditions":[]},'
289
+ '"displayFieldIds":["fld_xxx"],"lockRecord":true,"allowWithdraw":true,'
290
+ '"resubmit":{"enabled":true,"results":["rejected","withdrawn"],"limit":3},'
291
+ '"nodes":['
292
+ '{"id":"apn_trigger","type":"trigger"},'
293
+ '{"id":"apn_review","type":"approval","label":"Manager review",'
294
+ '"approverRules":[{"type":"employees","employeeIds":["emp_xxx"]}],'
295
+ '"mode":"all","selfPolicy":"skip","unavailablePolicy":"autoReject"},'
296
+ '{"id":"apn_approved","type":"approvedEnd"},'
297
+ '{"id":"apn_rejected","type":"rejectedEnd"}],'
298
+ '"edges":['
299
+ '{"id":"ape_1","source":"apn_trigger","target":"apn_review"},'
300
+ '{"id":"ape_2","source":"apn_review","target":"apn_approved","outcome":"approved"},'
301
+ '{"id":"ape_3","source":"apn_review","target":"apn_rejected","outcome":"rejected"}]}'
302
+ )),
303
+ Endpoint("approvals.update", "flowbase_approvals_update", "PUT", "/objects/{object_id}/approvals/{approval_id}", "metadata:write", "Update an approval flow graph.", "flowbase approvals update obj_xxx apr_xxx --json-file approval.json",
304
+ request_body=(
305
+ '{"baseRevision":4,"label":"Contract approval","enabled":true,'
306
+ '"trigger":{"manual":true,"create":false,"update":false,"match":"all","conditions":[]},'
307
+ '"displayFieldIds":["fld_xxx"],"lockRecord":true,"allowWithdraw":true,'
308
+ '"resubmit":{"enabled":true,"results":["rejected","withdrawn"],"limit":3},'
309
+ '"nodes":['
310
+ '{"id":"apn_trigger","type":"trigger"},'
311
+ '{"id":"apn_review","type":"approval","label":"Manager review",'
312
+ '"approverRules":[{"type":"employees","employeeIds":["emp_xxx"]}],'
313
+ '"mode":"all","selfPolicy":"skip","unavailablePolicy":"autoReject"},'
314
+ '{"id":"apn_approved","type":"approvedEnd"},'
315
+ '{"id":"apn_rejected","type":"rejectedEnd"}],'
316
+ '"edges":['
317
+ '{"id":"ape_1","source":"apn_trigger","target":"apn_review"},'
318
+ '{"id":"ape_2","source":"apn_review","target":"apn_approved","outcome":"approved"},'
319
+ '{"id":"ape_3","source":"apn_review","target":"apn_rejected","outcome":"rejected"}]}'
320
+ )),
321
+ Endpoint("approvals.reorder", "flowbase_approvals_reorder", "PUT", "/objects/{object_id}/approvals/order", "metadata:write", "Reorder object approval flows.", "flowbase approvals reorder obj_xxx --json '{\"baseRevision\":4,\"approvalIds\":[\"apr_a\",\"apr_b\"]}'",
322
+ request_body='{"baseRevision":4,"approvalIds":["apr_a","apr_b"]}'),
323
+ Endpoint("approvals.set-enabled", "flowbase_approvals_set_enabled", "POST", "/objects/{object_id}/approvals/{approval_id}/enabled", "metadata:write", "Enable or disable an approval flow.", "flowbase approvals set-enabled obj_xxx apr_xxx --base-revision 4 --enabled --yes", True,
324
+ request_body='{"baseRevision":4,"enabled":true}'),
325
+ Endpoint("approvals.delete", "flowbase_approvals_delete", "POST", "/objects/{object_id}/approvals/{approval_id}/delete", "metadata:write", "Logically delete an approval flow.", "flowbase approvals delete obj_xxx apr_xxx --base-revision 4 --yes", True,
326
+ request_body='{"baseRevision":4}'),
327
+ Endpoint("print-templates.list", "flowbase_print_templates_list", "GET", "/objects/{object_id}/print-templates", "metadata:read", "List object print templates.", "flowbase print-templates list obj_xxx"),
328
+ Endpoint("print-templates.get", "flowbase_print_templates_get", "GET", "/objects/{object_id}/print-templates/{template_id}", "metadata:read", "Get a print template with HTML and data code.", "flowbase print-templates get obj_xxx ptp_xxx"),
329
+ Endpoint("print-templates.create", "flowbase_print_templates_create", "POST", "/objects/{object_id}/print-templates", "metadata:write", "Create a print template draft.", "flowbase print-templates create obj_xxx --json-file template.json",
330
+ request_body=(
331
+ '{"baseRevision":3,"name":"Sales order","key":"sales_order","description":"",'
332
+ '"paper":{"size":"A4","width":null,"height":null,"orientation":"portrait",'
333
+ '"margins":{"top":15,"right":15,"bottom":15,"left":15}},'
334
+ '"header":{"left":"","center":"","right":""},'
335
+ '"footer":{"left":"","center":"Page {page} / {pages}","right":""},'
336
+ '"html":"<h1>{{ record.summary }}</h1>\\n<p>{{ data.total | money(\\"CNY\\") }}</p>",'
337
+ '"code":"import flowbase\\n\\ndef run():\\n return {\\"total\\": 0}"}'
338
+ )),
339
+ Endpoint("print-templates.update", "flowbase_print_templates_update", "PUT", "/objects/{object_id}/print-templates/{template_id}", "metadata:write", "Update a print template; omitted fields keep their value.", "flowbase print-templates update obj_xxx ptp_xxx --json-file template.json",
340
+ request_body=(
341
+ '{"baseRevision":4,"baseUpdatedAt":"2026-10-13T08:00:00+00:00",'
342
+ '"html":"<h1>{{ record.summary }}</h1>"}'
343
+ )),
344
+ Endpoint("print-templates.reorder", "flowbase_print_templates_reorder", "PUT", "/objects/{object_id}/print-templates/order", "metadata:write", "Reorder object print templates.", "flowbase print-templates reorder obj_xxx --json '{\"baseRevision\":4,\"templateIds\":[\"ptp_a\",\"ptp_b\"]}'",
345
+ request_body='{"baseRevision":4,"templateIds":["ptp_a","ptp_b"]}'),
346
+ Endpoint("print-templates.set-enabled", "flowbase_print_templates_set_enabled", "POST", "/objects/{object_id}/print-templates/{template_id}/enabled", "metadata:write", "Enable or disable a print template.", "flowbase print-templates set-enabled obj_xxx ptp_xxx --base-revision 4 --enabled --yes", True,
347
+ request_body='{"baseRevision":4,"enabled":true}'),
348
+ Endpoint("print-templates.delete", "flowbase_print_templates_delete", "POST", "/objects/{object_id}/print-templates/{template_id}/delete", "metadata:write", "Logically delete a print template.", "flowbase print-templates delete obj_xxx ptp_xxx --base-revision 4 --yes", True,
349
+ request_body='{"baseRevision":4}'),
350
+ Endpoint("pages.list", "flowbase_pages_list", "GET", "/pages", "metadata:read", "List custom Page metadata.", "flowbase pages list --object-id obj_xxx"),
351
+ Endpoint("pages.options", "flowbase_pages_options", "GET", "/pages/options", "metadata:read", "Get custom Page design options.", "flowbase pages options"),
352
+ Endpoint("pages.get", "flowbase_pages_get", "GET", "/pages/{page_id}", "metadata:read", "Get a custom Page.", "flowbase pages get pag_xxx"),
353
+ Endpoint("pages.create", "flowbase_pages_create", "POST", "/pages", "metadata:write", "Create a custom Page draft.", "flowbase pages create --json-file page.json",
354
+ request_body=(
355
+ '{"baseRevision":3,"page":{"key":"customer_create","label":"Create customer",'
356
+ '"description":"Create a customer record","objectId":"obj_xxx","type":"create",'
357
+ '"regions":[{"id":"tmp_reg_main","label":"基本信息","hideLabel":false,"columnRatios":[100],'
358
+ '"columns":[[{"id":"tmp_cmp_fields","label":"字段","hideLabel":true,'
359
+ '"grid":{"columns":2,"rows":1},"alignment":"left",'
360
+ '"labelPosition":"top","labelWidth":160,"fields":['
361
+ '{"id":"tmp_pfi_1","fieldId":"fld_name","sourcePath":[],'
362
+ '"row":1,"column":1,"columnSpan":1,"rowSpan":1},'
363
+ '{"id":"tmp_pfi_2","fieldId":"fld_stage","sourcePath":[],'
364
+ '"row":1,"column":2,"columnSpan":1,"rowSpan":1}]}]]}],"rules":[]}}'
365
+ )),
366
+ Endpoint("pages.clear-default", "flowbase_pages_clear_default", "POST", "/pages/{page_id}/clear-default", "metadata:write", "Clear this page's default assignment (also on deleted objects); fall back to the system page.", "flowbase pages clear-default pag_xxx --base-revision 4 --yes", True, request_body='{"baseRevision":4}'),
367
+ Endpoint("pages.update", "flowbase_pages_update", "PUT", "/pages/{page_id}", "metadata:write", "Update a custom Page draft.", "flowbase pages update pag_xxx --json-file page.json",
368
+ request_body=(
369
+ '{"baseRevision":4,"page":{"id":"pag_xxx","key":"customer_detail","label":"Customer detail",'
370
+ '"description":"Customer detail page","objectId":"obj_customer","type":"detail",'
371
+ '"regions":[{"id":"reg_xxx","label":"Overview","hideLabel":false,"columnRatios":[100],'
372
+ '"columns":[[{"kind":"detailSummary","id":"cmp_summary","label":"Customer summary",'
373
+ '"subtitleFieldId":"fld_email","statusFieldId":"fld_status",'
374
+ '"keyFieldIds":["fld_owner","fld_created_at"]},'
375
+ '{"label":"Customer context","hideLabel":false,'
376
+ '"grid":{"columns":2,"rows":1},"alignment":"left",'
377
+ '"labelPosition":"top","labelWidth":160,"fields":['
378
+ '{"fieldId":"fld_name","sourcePath":[],"row":1,"column":1,'
379
+ '"columnSpan":1,"rowSpan":1},'
380
+ '{"fieldId":"fld_account_name","sourcePath":['
381
+ '{"relationFieldId":"fld_account","targetObjectId":"obj_account"}],'
382
+ '"permission":"readonly","row":1,"column":2,"columnSpan":1,"rowSpan":1}]},'
383
+ '{"kind":"tabs","id":"cmp_tabs","label":"Customer details",'
384
+ '"defaultTabId":"tab_overview","tabs":['
385
+ '{"id":"tab_overview","name":"Overview","components":['
386
+ '{"kind":"stageBar","id":"cmp_stage","label":"Lifecycle",'
387
+ '"hideLabel":false,"fieldId":"fld_stage","allowDirectSwitch":true,"colorMode":"option"},'
388
+ '{"kind":"relatedSummary","id":"cmp_account","label":"Account summary",'
389
+ '"showTitle":true,"title":"Account","relationFieldId":"fld_account",'
390
+ '"summaryFieldIds":["fld_account_owner","fld_account_phone"]}]},'
391
+ '{"id":"tab_activity","name":"Activity","components":['
392
+ '{"kind":"attachmentCard","id":"cmp_contracts","label":"Contracts",'
393
+ '"showTitle":true,"title":"Contracts","fieldId":"fld_contracts",'
394
+ '"showMetadata":true,"editMode":"permission"},'
395
+ '{"kind":"noticeBlock","id":"cmp_notice","label":"Reminder",'
396
+ '"color":"yellow","showTitle":true,"title":"Before closing",'
397
+ '"body":"Confirm the signed contract before closing this customer."},'
398
+ '{"kind":"relatedList","id":"cmp_tickets","label":"Tickets","hideLabel":false,'
399
+ '"objectId":"obj_ticket","relationFieldId":"fld_customer",'
400
+ '"filters":[{"fieldId":"fld_status","operator":"neq","value":"done"}],'
401
+ '"columnFieldIds":["fld_title","fld_status","fld_created_at"],'
402
+ '"sort":{"fieldId":"fld_created_at","direction":"desc"},"limit":5,"showViewAll":true,'
403
+ '"createAction":{"enabled":true,"label":"New ticket"},'
404
+ '"carryIns":[{"fieldId":"fld_customer","source":"currentRecord"}]},'
405
+ '{"kind":"approval","id":"cmp_approval","label":"Approval","hideLabel":false}]}]}]]}],'
406
+ '"rules":[{"id":"rule_closed","name":"Lock and simplify closed customers",'
407
+ '"enabled":true,"match":"all","conditions":['
408
+ '{"fieldId":"fld_status","operator":"eq","value":"opt_closed"}],'
409
+ '"actions":[{"type":"setFieldState","fieldId":"fld_name","state":"readonly"},'
410
+ '{"type":"setComponentVisibility","componentId":"cmp_stage","state":"hidden"}]}]}}'
411
+ )),
412
+ Endpoint("pages.set-default", "flowbase_pages_set_default", "POST", "/pages/{page_id}/set-default", "metadata:write", "Set a default custom Page.", "flowbase pages set-default pag_xxx --base-revision 4 --yes", True,
413
+ request_body='{"baseRevision":4}'),
414
+ Endpoint("pages.delete", "flowbase_pages_delete", "POST", "/pages/{page_id}/delete", "metadata:write", "Logically delete a custom Page.", "flowbase pages delete pag_xxx --base-revision 4 --yes", True,
415
+ request_body='{"baseRevision":4}'),
416
+ Endpoint("pages.restore", "flowbase_pages_restore", "POST", "/pages/{page_id}/restore", "metadata:write", "Restore a custom Page.", "flowbase pages restore pag_xxx --base-revision 4 --yes", True,
417
+ request_body='{"baseRevision":4}'),
418
+ Endpoint("versions.list", "flowbase_versions_list", "GET", "/versions", "metadata:read", "List visible configuration versions.", "flowbase versions list"),
419
+ Endpoint("versions.get", "flowbase_versions_get", "GET", "/versions/{version_id}", "metadata:read", "Get a version with semantic and file diffs.", "flowbase versions show abc1234"),
420
+ Endpoint("releases.list", "flowbase_releases_list", "GET", "/releases", "metadata:read", "List production release records.", "flowbase versions releases", environments=("production",)),
421
+ Endpoint("changes.commit", "flowbase_changes_commit", "POST", "/changes/commit", "metadata:write", "Commit current test-environment drafts.", "flowbase changes commit --message 'Add customer field' --yes", True, environments=("test-a", "test-b"), audit_event="version.commit",
422
+ request_body='{"message":"Add customer field"}'),
423
+ Endpoint("changes.release", "flowbase_changes_release", "POST", "/changes/release", "metadata:write", "Release the current test-environment version.", "flowbase changes release --message 'Release customer field' --yes", True, environments=("test-a", "test-b"), audit_event="version.release",
424
+ request_body='{"message":"Release customer field"}'),
425
+ Endpoint(
426
+ "changes.direct-release-preflight",
427
+ "flowbase_changes_direct_release_preflight",
428
+ "POST",
429
+ "/changes/direct-release/preflight",
430
+ "metadata:write",
431
+ "Preflight a production direct release.",
432
+ "flowbase changes direct-release --message 'Fix production field' --yes",
433
+ environments=("production",),
434
+ ),
435
+ Endpoint(
436
+ "changes.direct-release",
437
+ "flowbase_changes_direct_release",
438
+ "POST",
439
+ "/changes/direct-release",
440
+ "metadata:write",
441
+ "Directly release production drafts.",
442
+ "flowbase changes direct-release --message 'Fix production field' --yes",
443
+ requires_confirmation=True,
444
+ environments=("production",),
445
+ preflight_operation_id="flowbase_changes_direct_release_preflight",
446
+ audit_event="version.forward-fix",
447
+ ),
448
+ Endpoint(
449
+ "environments.restart-preflight",
450
+ "flowbase_environments_restart_preflight",
451
+ "GET",
452
+ "/environments/restart/preflight",
453
+ "metadata:write",
454
+ "Preflight restarting the current test environment from production.",
455
+ "flowbase environments restart --confirm 'A 测试环境' --yes",
456
+ environments=("test-a", "test-b"),
457
+ ),
458
+ Endpoint(
459
+ "environments.restart",
460
+ "flowbase_environments_restart",
461
+ "POST",
462
+ "/environments/restart",
463
+ "metadata:write",
464
+ "Clear and restart the current test environment from production.",
465
+ "flowbase environments restart --confirm 'A 测试环境' --yes",
466
+ requires_confirmation=True,
467
+ environments=("test-a", "test-b"),
468
+ preflight_operation_id="flowbase_environments_restart_preflight",
469
+ audit_event="version.base-update",
470
+ ),
471
+ Endpoint("records.list", "flowbase_records_list", "GET", "/objects/{object_id}/records", "records:read", "List records.", "flowbase records list obj_xxx --page 1"),
472
+ Endpoint("records.get", "flowbase_records_get", "GET", "/objects/{object_id}/records/{record_id}", "records:read", "Get a record.", "flowbase records get obj_xxx rec_xxx"),
473
+ Endpoint("records.upload-attachment", "flowbase_records_upload_attachment", "POST", "/objects/{object_id}/fields/{field_id}/attachments", "records:write", "Upload an attachment before writing its ID to a record.", "flowbase records upload-attachment obj_xxx fld_xxx ./document.pdf"),
474
+ Endpoint("records.download-attachment", "flowbase_records_download_attachment", "GET", "/objects/{object_id}/records/{record_id}/attachments/{attachment_id}", "records:read", "Download an attachment bound to a record.", "flowbase records download-attachment obj_xxx rec_xxx att_xxx --output ./document.pdf"),
475
+ Endpoint("records.create", "flowbase_records_create", "POST", "/objects/{object_id}/records", "records:write", "Create a record. `values` keys are field keys (not IDs).", "flowbase records create obj_xxx --json-file record.json",
476
+ request_body=(
477
+ '{"values":{"name":"Acme Corp","stage":"opt_xxx","amount":12000.50,'
478
+ '"signed_on":"2026-09-01","is_vip":true,"account":"rec_xxx"}}'
479
+ )),
480
+ Endpoint("records.update", "flowbase_records_update", "PUT", "/objects/{object_id}/records/{record_id}", "records:write", "Update supplied field values on a record; omitted fields are preserved. `values` keys are field keys (not IDs).", "flowbase records update obj_xxx rec_xxx --json-file record.json",
481
+ request_body=(
482
+ '{"values":{"name":"Acme Corp","stage":"opt_xxx","amount":12000.50,'
483
+ '"signed_on":"2026-09-01","is_vip":true,"account":"rec_xxx"}}'
484
+ )),
485
+ Endpoint("records.delete", "flowbase_records_delete", "DELETE", "/objects/{object_id}/records/{record_id}", "records:write", "Delete a record.", "flowbase records delete obj_xxx rec_xxx --yes", True),
486
+ Endpoint(
487
+ "roles.list",
488
+ "flowbase_roles_list",
489
+ "GET",
490
+ "/roles",
491
+ "metadata:read",
492
+ "List organization roles.",
493
+ "flowbase roles list --page 1",
494
+ environments=("production",),
495
+ ),
496
+ Endpoint(
497
+ "roles.options",
498
+ "flowbase_roles_options",
499
+ "GET",
500
+ "/roles/options",
501
+ "metadata:read",
502
+ "List role editor option sources (apps and objects).",
503
+ "flowbase roles options",
504
+ environments=("production",),
505
+ ),
506
+ Endpoint(
507
+ "roles.get",
508
+ "flowbase_roles_get",
509
+ "GET",
510
+ "/roles/{role_id}",
511
+ "metadata:read",
512
+ "Get a role with permissions and members.",
513
+ "flowbase roles get rol_xxx",
514
+ environments=("production",),
515
+ ),
516
+ Endpoint(
517
+ "roles.create",
518
+ "flowbase_roles_create",
519
+ "POST",
520
+ "/roles",
521
+ "metadata:write",
522
+ "Create a custom role.",
523
+ "flowbase roles create --json-file role.json",
524
+ environments=("production",),
525
+ audit_event="role.created",
526
+ request_body=(
527
+ '{"name":"Sales","description":"Sales team",'
528
+ '"permissions":{"function":[],"apps":{"mode":"all"},'
529
+ '"objects":{"obj_xxx":{"view":true,"viewScope":"all","create":true,'
530
+ '"edit":true,"editScope":"all","delete":false}}}}'
531
+ ),
532
+ ),
533
+ Endpoint(
534
+ "roles.update",
535
+ "flowbase_roles_update",
536
+ "PUT",
537
+ "/roles/{role_id}",
538
+ "metadata:write",
539
+ "Update a custom role.",
540
+ "flowbase roles update rol_xxx --json-file role.json",
541
+ environments=("production",),
542
+ audit_event="role.updated",
543
+ request_body=(
544
+ '{"name":"Sales","description":"Sales team","updatedAt":"2026-01-01T00:00:00Z",'
545
+ '"permissions":{"function":[],"apps":{"mode":"all"},"objects":{}}}'
546
+ ),
547
+ ),
548
+ Endpoint(
549
+ "roles.delete",
550
+ "flowbase_roles_delete",
551
+ "DELETE",
552
+ "/roles/{role_id}",
553
+ "metadata:write",
554
+ "Delete a custom role.",
555
+ "flowbase roles delete rol_xxx --yes",
556
+ True,
557
+ environments=("production",),
558
+ audit_event="role.deleted",
559
+ ),
560
+ )
561
+
562
+ ENDPOINT_BY_KEY = {endpoint.key: endpoint for endpoint in ENDPOINTS}
563
+
564
+ # Closed-domain catalogs for offline authoring of complex request bodies.
565
+ # Keep in sync with server registries (field types, workflow/approval node kinds,
566
+ # permission points, dashboard filter presets, page component kinds).
567
+ # Icons intentionally reference the server file because the full Tabler set is large.
568
+ _PERMISSIONS = ("required", "optional", "readonly", "hidden")
569
+ _TEXT_FILTERS = ("eq", "neq", "contains", "not_contains", "is_empty", "is_not_empty")
570
+ _NUMBER_FILTERS = ("eq", "neq", "gt", "gte", "lt", "lte", "is_empty", "is_not_empty")
571
+ _DATE_FILTERS = ("eq", "before", "after", "lte", "gte", "is_empty", "is_not_empty")
572
+ _SINGLE_FILTERS = ("eq", "neq", "in", "not_in", "is_empty", "is_not_empty")
573
+ _MULTI_FILTERS = ("contains_any", "contains_all", "contains_none", "is_empty", "is_not_empty")
574
+ _BOOL_FILTERS = ("is_true", "is_false", "is_empty", "is_not_empty")
575
+ _EMPTY_FILTERS = ("is_empty", "is_not_empty")
576
+ _SELECT_OPTION = {
577
+ "id": "tmp_xxx",
578
+ "label": "Option label",
579
+ "lifecycle": {"status": "active"},
580
+ }
581
+
582
+
583
+ def _field_type(
584
+ type_name: str,
585
+ constraints: tuple[str, ...],
586
+ *,
587
+ writable: bool = True,
588
+ filter_operators: tuple[str, ...] = (),
589
+ record_value: str = "string | null",
590
+ extra: dict | None = None,
591
+ ) -> dict:
592
+ payload = {
593
+ "type": type_name,
594
+ "typeVersion": "v1",
595
+ "writable": writable,
596
+ "permissions": list(_PERMISSIONS),
597
+ "constraints": list(constraints),
598
+ "filterOperators": list(filter_operators),
599
+ "recordValue": record_value,
600
+ }
601
+ if extra:
602
+ payload.update(extra)
603
+ return payload
604
+
605
+
606
+ FIELD_TYPES = (
607
+ _field_type(
608
+ "text.singleLine", ("maxLength", "minLength", "placeholder", "unique"),
609
+ filter_operators=_TEXT_FILTERS, record_value="string | null",
610
+ extra={"createExample": {
611
+ "label": "Name", "key": "name", "description": "Display name",
612
+ "type": "text.singleLine", "typeVersion": "v1", "permission": "required",
613
+ "constraints": {"maxLength": 120},
614
+ }},
615
+ ),
616
+ _field_type(
617
+ "text.multiLine", ("maxLength", "minLength", "placeholder"),
618
+ filter_operators=_TEXT_FILTERS, record_value="string | null",
619
+ ),
620
+ _field_type(
621
+ "number", ("minimum", "maximum", "scale", "unique", "useThousands", "prefix", "suffix"),
622
+ filter_operators=_NUMBER_FILTERS, record_value="JSON number | null (not a quoted string)",
623
+ extra={"createExample": {
624
+ "label": "Capacity", "key": "capacity", "description": "Headcount cap",
625
+ "type": "number", "typeVersion": "v1", "permission": "optional",
626
+ "constraints": {"scale": 0, "minimum": 1, "maximum": 80},
627
+ }},
628
+ ),
629
+ _field_type(
630
+ "date", ("minimum", "maximum", "unique", "displayFormat"),
631
+ filter_operators=_DATE_FILTERS, record_value="YYYY-MM-DD | null",
632
+ ),
633
+ _field_type(
634
+ "select.single", ("options", "unique", "showColorDot"),
635
+ filter_operators=_SINGLE_FILTERS,
636
+ record_value="option id (opt_...), never the option label",
637
+ extra={
638
+ "optionShape": _SELECT_OPTION,
639
+ "createExample": {
640
+ "label": "Status", "key": "status", "description": "Lifecycle",
641
+ "type": "select.single", "typeVersion": "v1", "permission": "required",
642
+ "constraints": {"options": [
643
+ {"id": "tmp_open", "label": "Open", "lifecycle": {"status": "active"}},
644
+ {"id": "tmp_done", "label": "Done", "lifecycle": {"status": "active"}},
645
+ ]},
646
+ },
647
+ },
648
+ ),
649
+ _field_type(
650
+ "select.multiple", ("options", "maxSelections", "showColorDot"),
651
+ filter_operators=_MULTI_FILTERS, record_value="array of option ids | null",
652
+ extra={"optionShape": _SELECT_OPTION},
653
+ ),
654
+ _field_type(
655
+ "select.tree.single", ("nodes", "leafOnly", "unique", "showColorDot"),
656
+ filter_operators=_SINGLE_FILTERS, record_value="node id | null",
657
+ ),
658
+ _field_type(
659
+ "select.tree.multiple", ("nodes", "leafOnly", "maxSelections", "showColorDot"),
660
+ filter_operators=_MULTI_FILTERS, record_value="array of node ids | null",
661
+ ),
662
+ _field_type(
663
+ "lookup.record", ("targetObjectId", "displayFieldId", "unique"),
664
+ filter_operators=_SINGLE_FILTERS, record_value="record id (rec_...) | null",
665
+ extra={"createExample": {
666
+ "label": "Class", "key": "class", "description": "Owning class",
667
+ "type": "lookup.record", "typeVersion": "v1", "permission": "required",
668
+ "constraints": {"targetObjectId": "obj_xxx"},
669
+ }},
670
+ ),
671
+ _field_type(
672
+ "lookup.employee", ("isOwner", "unique"),
673
+ filter_operators=_SINGLE_FILTERS, record_value="employee id (emp_...) | null",
674
+ ),
675
+ _field_type(
676
+ "boolean", (),
677
+ filter_operators=_BOOL_FILTERS, record_value="true | false | null",
678
+ extra={"note": "View filters use is_true/is_false (no value). Page rules use eq with true/false."},
679
+ ),
680
+ _field_type(
681
+ "time", ("precision", "minimum", "maximum", "unique", "displayFormat"),
682
+ filter_operators=_DATE_FILTERS, record_value="HH:MM or HH:MM:SS | null",
683
+ ),
684
+ _field_type(
685
+ "datetime", ("minimum", "maximum", "unique", "displayFormat"),
686
+ filter_operators=_DATE_FILTERS, record_value="ISO-8601 datetime | null",
687
+ ),
688
+ _field_type(
689
+ "email", ("maxLength", "placeholder", "unique"),
690
+ filter_operators=_TEXT_FILTERS, record_value="email string | null",
691
+ ),
692
+ _field_type(
693
+ "phone", ("maxLength", "placeholder", "unique"),
694
+ filter_operators=_TEXT_FILTERS, record_value="phone string | null",
695
+ ),
696
+ _field_type(
697
+ "url", ("maxLength", "placeholder", "unique"),
698
+ filter_operators=_TEXT_FILTERS, record_value="URL string | null",
699
+ ),
700
+ _field_type(
701
+ "attachment", ("maxFileCount", "maxFileSize", "maxTotalSize", "allowedExtensions"),
702
+ filter_operators=_EMPTY_FILTERS, record_value="array of attachment ids (att_...)",
703
+ ),
704
+ _field_type(
705
+ "text.rich", ("maxLength", "placeholder"),
706
+ filter_operators=("contains", "not_contains", "is_empty", "is_not_empty"),
707
+ record_value="Markdown string | null",
708
+ ),
709
+ _field_type(
710
+ "autonumber", ("prefix", "start", "minDigits"),
711
+ writable=False, filter_operators=_TEXT_FILTERS, record_value="server-assigned string; omit on write",
712
+ extra={"createExample": {
713
+ "label": "Class no", "key": "class_no", "description": "Auto number",
714
+ "type": "autonumber", "typeVersion": "v1", "permission": "readonly",
715
+ "constraints": {"prefix": "CLS", "start": 2023001, "minDigits": 7},
716
+ }},
717
+ ),
718
+ _field_type(
719
+ "number.currency", ("currency", "minimum", "maximum", "scale", "currencyDisplay", "useThousands", "unique"),
720
+ filter_operators=_NUMBER_FILTERS, record_value="JSON number | null",
721
+ ),
722
+ _field_type(
723
+ "number.percent", ("minimum", "maximum", "scale", "unique"),
724
+ filter_operators=_NUMBER_FILTERS, record_value="JSON number | null",
725
+ ),
726
+ _field_type(
727
+ "duration", ("precision", "minimum", "maximum", "displayFormat", "unique"),
728
+ filter_operators=_NUMBER_FILTERS, record_value="integer duration | null",
729
+ ),
730
+ _field_type(
731
+ "rating", ("maxRating", "color"),
732
+ filter_operators=_NUMBER_FILTERS, record_value="integer 1..maxRating | null",
733
+ ),
734
+ _field_type(
735
+ "lookup.record.multiple", ("targetObjectId", "displayFieldId", "maxSelections"),
736
+ filter_operators=_MULTI_FILTERS, record_value="array of record ids | null",
737
+ ),
738
+ _field_type(
739
+ "lookup.department", (),
740
+ filter_operators=_SINGLE_FILTERS, record_value="department id (dep_...) | null",
741
+ ),
742
+ )
743
+
744
+ VIEW_FILTERS = {
745
+ "note": (
746
+ "View filters are not the same as page-rules. Read this catalog before views.create. "
747
+ "baseRevision comes from views.list / pages.list / the last write response — not objects.get."
748
+ ),
749
+ "shape": {"fieldId": "fld_...", "operator": "...", "value": "..."},
750
+ "valueOmittedFor": ("is_empty", "is_not_empty", "is_true", "is_false"),
751
+ "operatorsByFieldType": {
752
+ "text.singleLine,text.multiLine,email,phone,url,autonumber": _TEXT_FILTERS,
753
+ "text.rich": ("contains", "not_contains", "is_empty", "is_not_empty"),
754
+ "number,number.currency,number.percent,duration,rating": _NUMBER_FILTERS,
755
+ "date,time,datetime": _DATE_FILTERS,
756
+ "select.single,select.tree.single,lookup.record,lookup.employee,lookup.department": _SINGLE_FILTERS,
757
+ "select.multiple,select.tree.multiple,lookup.record.multiple": _MULTI_FILTERS,
758
+ "boolean": _BOOL_FILTERS,
759
+ "attachment": _EMPTY_FILTERS,
760
+ },
761
+ "valueShapes": {
762
+ "select.single": "option id from objects.get fields[].constraints.options[].id, not the label",
763
+ "lookup.record": "record id (rec_...)",
764
+ "lookup.employee": "employee id (emp_...)",
765
+ "boolean": "omit value; operator is_true or is_false",
766
+ "date": "YYYY-MM-DD",
767
+ "number": "JSON number",
768
+ },
769
+ }
770
+
771
+ RECORD_VALUES = {
772
+ "keys": "Field keys (name), never field ids (fld_...)",
773
+ "listVsGet": (
774
+ "records.list returns only the current view columns (often just the title field). "
775
+ "Use records.get for the full values object. Hidden fields are omitted from get."
776
+ ),
777
+ "omitOnWrite": ("autonumber", "record_id", "created_by", "updated_by", "created_at", "updated_at"),
778
+ "byType": {
779
+ "text.singleLine": "string",
780
+ "select.single": "option id (opt_...)",
781
+ "select.multiple": ["opt_a", "opt_b"],
782
+ "lookup.record": "rec_...",
783
+ "lookup.record.multiple": ["rec_a", "rec_b"],
784
+ "lookup.employee": "emp_...",
785
+ "boolean": True,
786
+ "date": "2026-09-01",
787
+ "number": 40,
788
+ "email": "ada@example.com",
789
+ "phone": "13800000000",
790
+ "text.rich": "Markdown string",
791
+ "attachment": ["att_..."],
792
+ },
793
+ }
794
+
795
+ AUTHORING = {
796
+ "globalFlags": {
797
+ "flags": ["--host", "--api-key", "--pretty", "--no-pretty", "--raw"],
798
+ "note": "These flags may appear before or after the subcommand.",
799
+ "examples": [
800
+ "flowbase --no-pretty objects list",
801
+ "flowbase objects list --no-pretty",
802
+ ],
803
+ },
804
+ "documentationKeys": "docs list returns endpoint keys. Use apps.objects.add for apps add-objects; docs show includes the CLI example.",
805
+ "requiredUpdateFields": {"apps.update": ["label", "icon", "color"]},
806
+ "requiredCreateFields": {
807
+ "objects.create": ["label", "key", "description"],
808
+ "fields.create": ["label", "key", "description", "type"],
809
+ "apps.create": ["label", "key"],
810
+ "views.create": ["baseRevision", "name", "defaultFieldIds"],
811
+ "pages.create": ["baseRevision", "page"],
812
+ "records.create": ["values"],
813
+ },
814
+ "draftRevision": {
815
+ "notReturnedBy": ["objects.get", "objects.list", "apps.get", "apps.list"],
816
+ "returnedBy": [
817
+ "views.list", "pages.list", "pages.get",
818
+ "every metadata write response (draftRevision)",
819
+ ],
820
+ "note": "Capture draftRevision from the last write or from views.list/pages.list before views/pages writes.",
821
+ },
822
+ "readThenWrite": (
823
+ "For update of views/pages/dashboards/fields with constraints: get the snapshot, "
824
+ "edit it, set baseRevision from draftRevision. fields.update.constraints is a full replace; "
825
+ "sending {} wipes options."
826
+ ),
827
+ "recordsNeedReleasedMetadata": (
828
+ "On production, records.create/list/get use the released runtime, not the current draft. "
829
+ "A newly created object returns HTTP 404 until changes.direct-release. "
830
+ "Author all metadata first, release, then seed records."
831
+ ),
832
+ "responseShapes": {
833
+ "organization.employees": "{items:[{id,name}],page,perPage,total,pages}; follow all pages.",
834
+ "organization.departments": "{items:[{id,name,parentId}]} (complete tree).",
835
+ "pages.evaluate": "{mode:rule-effects,visualRendering:false,recordValidation:false,draftRevision,matchedRules,fieldStates,componentVisibility}; states keyed by field/component IDs.",
836
+ "dashboards.run": "{ok,output,error,logs,durationMs,cached,mode:draft-preview}; inspect ok and error.",
837
+ "apps.create": "{appId,draftRevision}",
838
+ "dashboards.create": "{dashboardId,draftRevision}",
839
+ "workflows.create": "{workflowId,draftRevision}",
840
+ "approvals.create": "{approvalId,draftRevision}",
841
+ "print-templates.create": "{templateId,draftRevision}",
842
+ "pages.list": "Page documents are in items. The pages field is the pagination page count.",
843
+ "pages.create": "Returns {page:{id,key,...}, draftRevision}. There is no top-level pageId.",
844
+ "objects.create": "Returns {objectId, draftRevision}.",
845
+ "fields.create": "Returns {fieldId, draftRevision}.",
846
+ "records.create": "Returns {recordId}; use recordId with records.get to read the full record.",
847
+ "views.list": "Views are in items. draftRevision is on the envelope.",
848
+ "records.list": "Rows are in records. columns describes the current projection.",
849
+ },
850
+ "commonMistakes": [
851
+ {
852
+ "wrong": "flowbase apps list --no-pretty (old CLI rejected this)",
853
+ "right": "Global flags now work after the subcommand.",
854
+ },
855
+ {
856
+ "wrong": "objects.create without description",
857
+ "right": "label, key, and description are all required.",
858
+ },
859
+ {
860
+ "wrong": "fields.create with only --label/--key/--description",
861
+ "right": "Must include type via --json. See docs catalogs field-types.",
862
+ },
863
+ {
864
+ "wrong": "select options: [{\"label\":\"Won\"}]",
865
+ "right": "{\"id\":\"tmp_won\",\"label\":\"Won\",\"lifecycle\":{\"status\":\"active\"}}",
866
+ },
867
+ {
868
+ "wrong": "boolean view filter operator eq / value true",
869
+ "right": "{\"fieldId\":\"fld_xxx\",\"operator\":\"is_true\"}",
870
+ },
871
+ {
872
+ "wrong": "select/lookup filter or record value uses the display label",
873
+ "right": "Use opt_/rec_/emp_ ids from objects.get / records.get.",
874
+ },
875
+ {
876
+ "wrong": "records.list.values is the full record",
877
+ "right": "List is a column projection. records.get for all field keys.",
878
+ },
879
+ {
880
+ "wrong": "records values keyed by fld_ ids",
881
+ "right": "values keys are field keys (name), not ids.",
882
+ },
883
+ {
884
+ "wrong": "pages.list.pages is the page list",
885
+ "right": "Use items. pages is the pagination page count.",
886
+ },
887
+ {
888
+ "wrong": "pages.create with empty columns: [[]]",
889
+ "right": "Place a field component. See docs example pages.create.",
890
+ },
891
+ {
892
+ "wrong": "fields.update constraints: {}",
893
+ "right": "constraints replaces the whole set. Copy options from objects.get.",
894
+ },
895
+ {
896
+ "wrong": "fields.create defaultValue: null",
897
+ "right": "Omit defaultValue unless it is a real value. null is treated as an explicit invalid default.",
898
+ },
899
+ {
900
+ "wrong": "two fields on one object with the same label",
901
+ "right": "label and key are both unique per object. Rename the new field (e.g. 所属班级 vs 班级).",
902
+ },
903
+ {
904
+ "wrong": "pages.create page.description is an empty string",
905
+ "right": "description must be 1-500 characters. Do not send \"\".",
906
+ },
907
+ {
908
+ "wrong": "region columnRatios: [1] or [1,1]",
909
+ "right": "columnRatios are percents that must sum to 100, e.g. [100] or [50,50].",
910
+ },
911
+ {
912
+ "wrong": "records.create on a brand-new object before release",
913
+ "right": "Production records APIs see released metadata only. flowbase changes direct-release --yes first.",
914
+ },
915
+ {
916
+ "wrong": "pages.create response pageId",
917
+ "right": "Read page.id from the page object and draftRevision from the envelope.",
918
+ },
919
+ {
920
+ "wrong": "read a hidden field from records.get.values",
921
+ "right": "Hidden fields are omitted from records.get. Unhide (and release on production) before reading them.",
922
+ },
923
+ {
924
+ "wrong": "objects.get.draftRevision",
925
+ "right": "Read draftRevision from views.list, pages.list, or the last write.",
926
+ },
927
+ {
928
+ "wrong": "dashboard date_range filter with no default",
929
+ "right": "Unset date_range defaults to last_30_days at runtime; set default.preset or omit the filter.",
930
+ },
931
+ ],
932
+ }
933
+
934
+ WORKFLOW_NODE_TYPES = (
935
+ {"type": "trigger", "cardinality": "exactly 1", "in": 0, "out": 1,
936
+ "note": "entry node; event in workflow trigger, not per node",
937
+ "conditionPathKeys": ("fieldId", "relationFieldId"),
938
+ "valueReferenceKeys": ("valueRef", "valueRefRelationFieldId")},
939
+ {"type": "branch", "cardinality": ">= 1 branches", "in": 1, "out": "1 per branch (+ defaultBranchId)",
940
+ "note": "each branch needs an outgoing edge carrying branchId",
941
+ "conditionPathKeys": ("fieldId", "relationFieldId"),
942
+ "valueReferenceKeys": ("valueRef", "valueRefRelationFieldId")},
943
+ {"type": "updateSelf", "cardinality": ">= 0", "in": 1, "out": 1,
944
+ "note": "assignments set/clear fields on the triggering record",
945
+ "valueReferenceKeys": ("valueRef", "valueRefRelationFieldId")},
946
+ {"type": "updateRelated", "cardinality": ">= 0", "in": 1, "out": 1,
947
+ "requiredKeys": ("relationFieldId", "assignments"),
948
+ "note": "relationFieldId must be an active single-record lookup on the workflow object; assignment fieldId targets that related object",
949
+ "valueReferenceKeys": ("valueRef", "valueRefRelationFieldId")},
950
+ {"type": "end", "cardinality": ">= 1", "in": "any", "out": 0,
951
+ "note": "terminal node"},
952
+ )
953
+
954
+ APPROVAL_NODE_TYPES = (
955
+ {"type": "trigger", "cardinality": "exactly 1", "in": 0, "out": 1,
956
+ "note": "entry node; initiation modes live on the flow trigger, not the node"},
957
+ {"type": "approval", "cardinality": ">= 1", "in": 1, "out": 2,
958
+ "note": "requires approved + rejected outcome edges; approverRules / mode / "
959
+ "selfPolicy / unavailablePolicy"},
960
+ {"type": "branch", "cardinality": ">= 0", "in": 1, "out": "1 per branch (+ defaultBranchId)",
961
+ "note": "same condition protocol as workflows; each branchId needs an edge"},
962
+ {"type": "updateRecord", "cardinality": ">= 0", "in": 1, "out": 1,
963
+ "note": "assignments update the locked record as system actor"},
964
+ {"type": "approvedEnd", "cardinality": ">= 1", "in": "any", "out": 0,
965
+ "note": "terminal approved outcome"},
966
+ {"type": "rejectedEnd", "cardinality": ">= 1", "in": "any", "out": 0,
967
+ "note": "terminal rejected outcome"},
968
+ )
969
+
970
+ FUNCTION_PERMISSIONS = (
971
+ "workspace.access", "app.manage", "object.manage", "dashboard.manage",
972
+ "version.manage", "org.manage", "system.manage",
973
+ )
974
+
975
+ DATA_SCOPES = ("all", "own_created", "own_responsible", "own_and_subordinates", "own_department_tree")
976
+
977
+ DASHBOARD_FILTER_TYPES = ("date_range", "field_select")
978
+ DASHBOARD_DATE_RANGE_PRESETS = (
979
+ "today", "yesterday", "last_7_days", "last_30_days",
980
+ "this_week", "this_month", "this_quarter", "this_year",
981
+ )
982
+
983
+ # Page authoring catalogs mirror the strict public Page schema and semantic
984
+ # constraints. Field components omit `kind`; every other component sets it.
985
+ PAGE_COMPONENTS = {
986
+ "note": (
987
+ "Region columns hold Page components. All kinds except field are detail-only. "
988
+ "Use pages.options for object/field candidates and pages.get for stable page, "
989
+ "region, component, and tab IDs."
990
+ ),
991
+ "authoringWorkflow": {
992
+ "read": "flowbase pages get pag_xxx",
993
+ "update": "flowbase pages update pag_xxx --json-file page.json",
994
+ "envelope": {"baseRevision": "<pages.get draftRevision>", "page": "<editable page snapshot>"},
995
+ "removeReadOnlyPageKeys": (
996
+ "objectLabel", "objectStatus", "lifecycle", "isDefault",
997
+ "defaultPageId", "defaultPageLabel",
998
+ ),
999
+ "note": (
1000
+ "pages.get is a read projection, not a directly reusable update body. "
1001
+ "Preserve authoring IDs and fields, map draftRevision to baseRevision, "
1002
+ "and remove the listed read-only Page keys before pages.update."
1003
+ ),
1004
+ },
1005
+ "common": {
1006
+ "pageTypes": ("create", "detail"),
1007
+ "description": "page.description is required and must be 1-500 characters; do not send \"\".",
1008
+ "columnRatios": "Positive integers that must sum to 100, e.g. [100] or [60,40]. [1] is invalid.",
1009
+ "componentId": (
1010
+ "Preserve stable cmp_ IDs from pages.get. Omit id or use a tmp_ ID for a new "
1011
+ "component; save and read the Page again before a rule targets that component."
1012
+ ),
1013
+ "exampleIds": (
1014
+ "Every pag_/reg_/cmp_/tab_/rule_/obj_/fld_ value in examples is a placeholder; "
1015
+ "replace stable resource IDs with values returned by pages.get/pages.options."
1016
+ ),
1017
+ "detailOnlyKinds": (
1018
+ "relatedList", "approval", "detailSummary", "stageBar", "tabs",
1019
+ "relatedSummary", "attachmentCard", "noticeBlock",
1020
+ ),
1021
+ },
1022
+ "kinds": (
1023
+ {
1024
+ "kind": None,
1025
+ "name": "field",
1026
+ "note": "Omit kind. Each placed field carries its own sourcePath; sourceObjectId is legacy compatibility input only.",
1027
+ "requiredKeys": (
1028
+ "label", "hideLabel", "grid",
1029
+ "alignment", "labelPosition", "labelWidth", "fields",
1030
+ ),
1031
+ "optionalKeys": ("id", "sourceObjectId"),
1032
+ "placedField": {
1033
+ "requiredKeys": (
1034
+ "fieldId", "row", "column", "columnSpan", "rowSpan",
1035
+ ),
1036
+ "optionalKeys": ("id", "sourcePath", "permission"),
1037
+ "sourcePathHop": {
1038
+ "requiredKeys": ("relationFieldId", "targetObjectId"),
1039
+ "maxHops": 2,
1040
+ "note": "An empty path means the Page object. Every hop must use an active lookup.record field and declare its target object.",
1041
+ },
1042
+ },
1043
+ },
1044
+ {
1045
+ "kind": "relatedList",
1046
+ "name": "related record list",
1047
+ "pageTypes": ("detail",),
1048
+ "requiredKeys": (
1049
+ "kind", "label", "hideLabel", "objectId", "relationFieldId",
1050
+ "columnFieldIds", "sort", "limit", "showViewAll", "createAction",
1051
+ ),
1052
+ "optionalKeys": ("id", "filters", "carryIns"),
1053
+ "limit": (5, 10, 20),
1054
+ "sortDirections": ("asc", "desc"),
1055
+ "carryInSources": (
1056
+ {
1057
+ "source": "currentRecord",
1058
+ "note": "Value is the current record id; target field must be "
1059
+ "lookup.record pointing at the page object. When create "
1060
+ "is enabled, relationFieldId must appear exactly once "
1061
+ "with this source.",
1062
+ },
1063
+ {
1064
+ "source": "currentRecordField",
1065
+ "requires": "sourceFieldId",
1066
+ "note": "Copy an active field from the current record; source "
1067
+ "and target field types must match.",
1068
+ },
1069
+ ),
1070
+ "filters": {
1071
+ "shape": {"fieldId": "fld_...", "operator": "...", "value": "..."},
1072
+ "note": "Same filter protocol as object views; AND-merged with the "
1073
+ "implicit relationFieldId eq currentRecord condition.",
1074
+ },
1075
+ },
1076
+ {
1077
+ "kind": "approval",
1078
+ "name": "record approval",
1079
+ "pageTypes": ("detail",),
1080
+ "maxPerPage": 1,
1081
+ "requiredKeys": ("kind", "label", "hideLabel"),
1082
+ "optionalKeys": ("id",),
1083
+ "note": (
1084
+ "Reference-free presentation slot for submit / status / history. "
1085
+ "Runtime data comes from employee APIs, not OpenAPI/CLI."
1086
+ ),
1087
+ },
1088
+ {
1089
+ "kind": "detailSummary",
1090
+ "name": "detail summary",
1091
+ "pageTypes": ("detail",),
1092
+ "placement": ("topLevel",),
1093
+ "maxPerPage": 1,
1094
+ "requiredKeys": ("kind", "label", "keyFieldIds"),
1095
+ "optionalKeys": ("id", "subtitleFieldId", "statusFieldId"),
1096
+ "constraints": {
1097
+ "labelLength": (1, 50),
1098
+ "keyFieldCount": (1, 8),
1099
+ "fields": "active fields on the Page object; subtitle/status/key fields must not repeat",
1100
+ "statusFieldTypes": ("select.single", "select.tree.single", "boolean"),
1101
+ },
1102
+ },
1103
+ {
1104
+ "kind": "stageBar",
1105
+ "name": "stage bar",
1106
+ "pageTypes": ("detail",),
1107
+ "placement": ("topLevel", "tabs"),
1108
+ "requiredKeys": (
1109
+ "kind", "label", "hideLabel", "fieldId",
1110
+ "allowDirectSwitch", "colorMode",
1111
+ ),
1112
+ "optionalKeys": ("id",),
1113
+ "colorModes": ("option", "standard"),
1114
+ "constraints": {
1115
+ "field": "active select.single field on the Page object with at least two active options",
1116
+ "uniqueBinding": "a fieldId may be used by only one stageBar in the whole Page",
1117
+ },
1118
+ },
1119
+ {
1120
+ "kind": "tabs",
1121
+ "name": "tabs",
1122
+ "pageTypes": ("detail",),
1123
+ "placement": ("topLevelSingleColumnRegion",),
1124
+ "maxPerPage": 1,
1125
+ "requiredKeys": ("kind", "label", "defaultTabId", "tabs"),
1126
+ "optionalKeys": ("id",),
1127
+ "tab": {
1128
+ "requiredKeys": ("name", "components"),
1129
+ "optionalKeys": ("id",),
1130
+ "id": "Existing tabs use stable tab_ IDs; omit id only for a new tab.",
1131
+ "count": (2, 8),
1132
+ "nameLength": (1, 20),
1133
+ "nameUnique": True,
1134
+ "minimumComponents": 1,
1135
+ "allowedChildKinds": (
1136
+ "field", "relatedList", "approval", "stageBar",
1137
+ "relatedSummary", "attachmentCard", "noticeBlock",
1138
+ ),
1139
+ },
1140
+ "note": "defaultTabId must reference a tab in this component; tabs cannot be nested.",
1141
+ },
1142
+ {
1143
+ "kind": "relatedSummary",
1144
+ "name": "related object summary",
1145
+ "pageTypes": ("detail",),
1146
+ "placement": ("topLevel", "tabs"),
1147
+ "requiredKeys": (
1148
+ "kind", "label", "showTitle", "title",
1149
+ "relationFieldId", "summaryFieldIds",
1150
+ ),
1151
+ "optionalKeys": ("id",),
1152
+ "constraints": {
1153
+ "relationField": "active lookup.record field on the Page object",
1154
+ "summaryFieldCount": (1, 6),
1155
+ "summaryFields": (
1156
+ "active direct fields on the target object; attachment, text.rich, "
1157
+ "lookup.record, and lookup.record.multiple are not supported"
1158
+ ),
1159
+ "titleLengthWhenShown": (1, 50),
1160
+ },
1161
+ },
1162
+ {
1163
+ "kind": "attachmentCard",
1164
+ "name": "attachment card",
1165
+ "pageTypes": ("detail",),
1166
+ "placement": ("topLevel", "tabs"),
1167
+ "requiredKeys": (
1168
+ "kind", "label", "showTitle", "title", "fieldId",
1169
+ "showMetadata", "editMode",
1170
+ ),
1171
+ "optionalKeys": ("id",),
1172
+ "editModes": ("permission", "readonly"),
1173
+ "constraints": {
1174
+ "field": "active attachment field on the Page object",
1175
+ "titleLengthWhenShown": (1, 50),
1176
+ },
1177
+ },
1178
+ {
1179
+ "kind": "noticeBlock",
1180
+ "name": "notice block",
1181
+ "pageTypes": ("detail",),
1182
+ "placement": ("topLevel", "tabs"),
1183
+ "requiredKeys": (
1184
+ "kind", "label", "color", "showTitle", "title", "body",
1185
+ ),
1186
+ "optionalKeys": ("id",),
1187
+ "colors": ("yellow", "green", "blue", "pink", "purple", "gray"),
1188
+ "constraints": {
1189
+ "titleLengthWhenShown": (1, 50),
1190
+ "bodyLength": (1, 500),
1191
+ "bodyFormat": "plain text only; no Markdown, HTML, URLs, variables, or templates",
1192
+ },
1193
+ },
1194
+ ),
1195
+ }
1196
+
1197
+ PAGE_RULES = {
1198
+ "note": (
1199
+ "Rules are evaluated in list order. Enabled matching rules apply their actions "
1200
+ "in order; the last matching action wins independently per fieldId or componentId."
1201
+ ),
1202
+ "rule": {
1203
+ "requiredKeys": ("name", "conditions", "actions"),
1204
+ "optionalKeys": ("id", "enabled", "match"),
1205
+ "defaults": {"enabled": True, "match": "all"},
1206
+ "matchValues": ("all", "any"),
1207
+ },
1208
+ "condition": {
1209
+ "requiredKeys": ("fieldId", "operator"),
1210
+ "optionalKeys": ("value",),
1211
+ "shape": {"fieldId": "fld_...", "operator": "eq", "value": "..."},
1212
+ "fieldCandidates": (
1213
+ "active Page-object fields placed in ordinary field components with sourcePath=[]; "
1214
+ "fields referenced only by business components and cross-object fields are excluded"
1215
+ ),
1216
+ "valueOmittedFor": ("is_empty", "is_not_empty"),
1217
+ },
1218
+ "operatorsByFieldType": {
1219
+ "text.singleLine,text.multiLine,text.rich,email,phone,url,autonumber": (
1220
+ "eq", "neq", "contains", "not_contains", "is_empty", "is_not_empty",
1221
+ ),
1222
+ "number,number.currency,number.percent,duration,rating": (
1223
+ "eq", "neq", "gt", "gte", "lt", "lte", "is_empty", "is_not_empty",
1224
+ ),
1225
+ "date,time,datetime": ("eq", "before", "after", "is_empty", "is_not_empty"),
1226
+ "select.single,select.tree.single,lookup.employee,lookup.record": (
1227
+ "eq", "neq", "is_empty", "is_not_empty",
1228
+ ),
1229
+ "select.multiple,select.tree.multiple": (
1230
+ "contains", "not_contains", "is_empty", "is_not_empty",
1231
+ ),
1232
+ "boolean": ("eq", "is_empty", "is_not_empty"),
1233
+ "attachment": ("is_empty", "is_not_empty"),
1234
+ "lookup.record.multiple": (
1235
+ "contains_any", "contains_all", "contains_none", "is_empty", "is_not_empty",
1236
+ ),
1237
+ "lookup.department": ("eq", "neq", "in", "not_in", "is_empty", "is_not_empty"),
1238
+ },
1239
+ "actions": (
1240
+ {
1241
+ "type": "setFieldState",
1242
+ "pageTypes": ("create", "detail"),
1243
+ "requiredKeys": ("type", "fieldId", "state"),
1244
+ "states": ("required", "optional", "readonly", "hidden"),
1245
+ },
1246
+ {
1247
+ "type": "setComponentVisibility",
1248
+ "pageTypes": ("detail",),
1249
+ "requiredKeys": ("type", "componentId", "state"),
1250
+ "states": ("visible", "hidden"),
1251
+ "targetKinds": (
1252
+ "relatedList", "approval", "detailSummary", "stageBar", "tabs",
1253
+ "relatedSummary", "attachmentCard", "noticeBlock",
1254
+ ),
1255
+ "excludedTargets": ("field", "region", "tab", "button", "systemArea"),
1256
+ "sameRuleUniqueTarget": True,
1257
+ "note": (
1258
+ "Targets use stable cmp_ IDs from pages.get, including components inside tabs. "
1259
+ "A newly added component must be saved and read back before adding this action. "
1260
+ "visible cannot override permissions, invalid references, or component availability."
1261
+ ),
1262
+ },
1263
+ ),
1264
+ }
1265
+
1266
+ RESOURCE_ID_PREFIXES = {
1267
+ "app_": "app", "grp_": "app group", "obj_": "object", "fld_": "field",
1268
+ "viw_": "view", "wfl_": "workflow", "apr_": "approval flow", "pag_": "custom page",
1269
+ "reg_": "page region", "cmp_": "page component",
1270
+ "dsh_": "dashboard", "dcm_": "dashboard component", "rol_": "role",
1271
+ "rec_": "record", "att_": "attachment", "emp_": "employee", "dep_": "department",
1272
+ }
1273
+
1274
+ WORKFLOW_BRANCHES = {
1275
+ "shape": {"id": "tmp_branch", "type": "branch", "label": "Amount", "branches": [{"id": "tmp_high", "name": "High", "match": "all", "conditions": [{"fieldId": "fld_amount", "operator": "gt", "value": 10}]}], "defaultBranchId": "tmp_other"},
1276
+ "edges": [{"source": "tmp_branch", "target": "tmp_update", "branchId": "tmp_high"}, {"source": "tmp_branch", "target": "tmp_end", "branchId": "tmp_other"}],
1277
+ "note": "Add exactly one trigger and an end; connect every branch and the default to a reachable node. Read fields/relations from workflows list. Preserve node/edge IDs on update; condition IDs are field IDs, not field keys.",
1278
+ "updateEnvelope": ["baseRevision", "label", "enabled", "trigger", "nodes", "edges"],
1279
+ }
1280
+
1281
+ APPROVAL_RULES = {
1282
+ "sources": [
1283
+ {"type": "employees", "employeeIds": ["emp_from_organization_employees"]},
1284
+ {"type": "recordField", "fieldId": "fld_active_lookup_employee"},
1285
+ {"type": "submitterSupervisor", "level": 1},
1286
+ ],
1287
+ "discovery": "organization employees --query NAME --page N returns real IDs; follow pages until complete. objects get supplies lookup.employee fields. Do not invent employee IDs.",
1288
+ "modes": ["all", "any", "sequential"],
1289
+ "selfPolicy": ["allow", "skip"],
1290
+ "unavailablePolicy": ["autoApprove", "autoReject", "fallback"],
1291
+ "constraints": "employees needs nonempty employeeIds; recordField needs an active lookup.employee; supervisor level is 1..10; fallback needs fallbackEmployeeId from employee discovery.",
1292
+ "resubmit": "{enabled,results,limit}: results subset of rejected/withdrawn/approved (nonempty when enabled); limit null (unlimited) or integer 2..99 counting every non-failed instance per record. Omitted means enabled, all results, unlimited.",
1293
+ "updateEnvelope": ["baseRevision", "label", "enabled", "trigger", "displayFieldIds", "lockRecord", "allowWithdraw", "resubmit", "nodes", "edges"],
1294
+ "note": "Use docs example approvals.create for a complete graph. Start disabled while authoring. Employee submit/decide is not a management CLI operation.",
1295
+ }
1296
+
1297
+ PAGE_BUTTONS = {
1298
+ "location": "page.buttons on a detail page; preserve stable btn_ IDs on update, omit id for a new button.",
1299
+ "example": {"name": "Mark done", "color": "#2563EB", "description": "Mark this record completed", "enabled": True, "match": "all", "conditions": [], "action": {"type": "autoAssign", "confirm": True, "confirmText": "Mark completed?", "assignments": [{"fieldId": "fld_done", "op": "set", "source": "fixed", "value": True}]}},
1300
+ "assignment": {"op": ["set", "clear"], "source": ["fixed", "currentField", "currentEmployee", "currentTime"], "note": "fixed uses value; currentField uses sourceFieldId; clear omits source/value. Use active writable field IDs from objects get; computed/system fields are not targets."},
1301
+ "conditions": "Same condition shapes/operators as page-rules; an empty list shows an enabled button unconditionally. match is all or any.",
1302
+ "note": "Read pages get; retain regions, rules and existing buttons. Remove read-only page projection keys listed in page-components and wrap as {baseRevision,page}. pages evaluate does not execute buttons.",
1303
+ }
1304
+
1305
+ DASHBOARD_RUNTIME = {
1306
+ "command": "flowbase dashboards run dsh_xxx dcm_xxx --json '{\"filterValues\":{}}'",
1307
+ "ids": "Read dashboards get for stable component IDs and filters.",
1308
+ "querySDK": {
1309
+ "namespace": "flowbase.query.records",
1310
+ "count": "count(object, where=None, scope=None, inherit_filters=False) -> integer",
1311
+ "group_by": "group_by(object, group_field, metric='count', metric_field=None, where=None, scope=None, inherit_filters=False) -> [{value,label,metric}]",
1312
+ "timeseries": "timeseries(object, time_field=None, interval='day', metric='count', metric_field=None, where=None, scope=None, inherit_filters=False) -> [{bucket,metric}]",
1313
+ "list": "list(object, fields=None, where=None, order_by=None, limit=100, scope=None, inherit_filters=False) -> {fields,rows,labels,total}",
1314
+ "keys": "Use object and field KEYS from objects list/get, not IDs. order_by is a field key, with '-' for descending; default is -created_at.",
1315
+ "where": [{"field": "amount", "operator": "gt", "value": 10}],
1316
+ "constraints": "where conditions use AND and each field's supported filter operators. Aggregation metric: count/sum/avg/max/min; non-count needs a numeric metric_field. inherit_filters defaults false. scope only accepts own or null; API keys have no employee identity and own does not narrow their all-record grant.",
1317
+ "limits": "20 queries per component, list limit 1..500, at most 100 groups; hidden fields are unavailable. No arbitrary SQL, network or imports beyond flowbase. Sandbox execution limit 15 seconds.",
1318
+ },
1319
+ "recordExample": "import flowbase\n\ndef run():\n total = flowbase.query.records.count(object=\"customer\")\n return {\"type\": \"metric\", \"value\": total}\n",
1320
+ "recordExampleSetup": "Replace customer with an active object key from objects list; create a record and run this code as a saved component to verify the count. Use dashboards.update preserving the other components, then dashboards run with IDs from dashboards get.",
1321
+ "mode": "Draft preview, requiring metadata:read and records:read. No employee impersonation; current employee context is empty for API keys.",
1322
+ "result": "{ok,output,error,logs,durationMs,cached}; check ok, not just HTTP success. Runs are read-only; output is not a visual rendering.",
1323
+ "example": "import flowbase\n\ndef run():\n return {\"type\": \"metric\", \"value\": 1}\n",
1324
+ }
1325
+
1326
+ CATALOGS = {
1327
+ "workflow-branches": WORKFLOW_BRANCHES,
1328
+ "approval-rules": APPROVAL_RULES,
1329
+ "page-buttons": PAGE_BUTTONS,
1330
+ "dashboard-runtime": DASHBOARD_RUNTIME,
1331
+ "field-types": FIELD_TYPES,
1332
+ "view-filters": VIEW_FILTERS,
1333
+ "record-values": RECORD_VALUES,
1334
+ "authoring": AUTHORING,
1335
+ "workflow-nodes": WORKFLOW_NODE_TYPES,
1336
+ "approval-nodes": APPROVAL_NODE_TYPES,
1337
+ "permissions": {"function": FUNCTION_PERMISSIONS, "dataScopes": DATA_SCOPES},
1338
+ "dashboard-filters": {
1339
+ "types": DASHBOARD_FILTER_TYPES,
1340
+ "dateRangePresets": DASHBOARD_DATE_RANGE_PRESETS,
1341
+ "note": (
1342
+ "A date_range filter without default falls back to last_30_days at runtime. "
1343
+ "Always set default.preset (or a custom range) when historical records must stay visible."
1344
+ ),
1345
+ },
1346
+ "page-components": PAGE_COMPONENTS,
1347
+ "page-rules": PAGE_RULES,
1348
+ "id-prefixes": RESOURCE_ID_PREFIXES,
1349
+ "icons": {
1350
+ "note": "6146 Tabler icon names are supported; any Icon* name from the "
1351
+ "server's object_icon_names.json is valid.",
1352
+ "examples": ["IconCube", "IconDatabase", "IconChartPie", "IconApps",
1353
+ "IconFolder", "IconTable", "IconUsers", "IconSettings",
1354
+ "IconSchool", "IconUsersGroup", "IconBuildingCommunity"],
1355
+ },
1356
+ }
1357
+
1358
+ PENDING_MANAGEMENT_OPERATIONS = (
1359
+ {"key": "environments.enable", "status": "pending", "reason": "Requires an independent capability and confirmation contract."},
1360
+ {"key": "changes.clear-drafts", "status": "pending", "reason": "Requires an independent capability and confirmation contract."},
1361
+ {"key": "api-keys.manage", "status": "pending", "reason": "API keys cannot delegate or expand their own authority."},
1362
+ {"key": "git-credentials.manage", "status": "pending", "reason": "Requires a dedicated credential-management capability."},
1363
+ {"key": "metadata.purge", "status": "pending", "reason": "Physical purge is not delivered."},
1364
+ )