flowbase-cli 0.1.14__tar.gz → 0.1.16__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flowbase-cli
3
- Version: 0.1.14
3
+ Version: 0.1.16
4
4
  Summary: Command line client for the FlowBase OpenAPI
5
5
  Author: DeepFlowAI
6
6
  License: Apache-2.0
@@ -160,6 +160,14 @@ flowbase objects cli-edit-policy update obj_xxx --json '{"baseRevision":4,"polic
160
160
 
161
161
  `records create` returns `{recordId}`; use it with `records get`.
162
162
 
163
+ To add existing objects to an application, pass their IDs with `--object-ids`:
164
+
165
+ ```bash
166
+ flowbase apps add-objects app_xxx --object-ids obj_a,obj_b
167
+ ```
168
+
169
+ Use a comma-separated list for multiple objects. Object IDs are not positional arguments. Run `flowbase docs show apps.objects.add` for the endpoint details.
170
+
163
171
  Record `values` keys are **field keys** (`name`), not internal IDs (`fld_...`). IDs are `obj_`/`fld_`/`dsh_`/`wfl_`/`pag_`/`reg_`/`cmp_`-prefixed; see `docs catalogs id-prefixes`.
164
172
 
165
173
  ### 6.2 Edit and publish configuration on a test environment
@@ -280,3 +288,77 @@ flowbase roles update rol_xxx --json-file role.json
280
288
  - `docs catalogs page-buttons`, `workflow-branches`, `approval-rules`, and `dashboard-runtime` describe complex configuration and runtime limits.
281
289
 
282
290
  Transport requests time out after 60 seconds. A lost write response has an unknown outcome: read the resource and current revision before retrying. The CLI never automatically retries writes.
291
+
292
+ ### Recent feature authoring
293
+
294
+ - `flowbase docs catalogs planning`: tree/Gantt view snapshots, candidate queries and optimistic plan updates. Read a full view with `views list` before updating; preserve `displayMode`, `parentFieldId` and `gantt`.
295
+ - `flowbase docs catalogs record-bundles`: initialize, atomically submit and recover a parent/detail submission. Reuse a submission ID only with the identical request body.
296
+ - `flowbase docs catalogs formula-authoring`: formula/rollup sources, validation, saved-record trials and runtime draft preview. Derived values are read-only.
297
+ - `flowbase docs catalogs workflow-python`: record creation examples and failed-task retry. `workflows python-runtime OBJECT new` discovers current object bindings; `workflows executions` includes Python attempts, logs and `canRetry`.
298
+
299
+ Use `records list OBJECT --view-id VIEW --view-layout` for tree/Gantt projections; the default remains a flat list. `records update` accepts `updatedAt` and `planViewId` in its JSON body for version-checked plan edits. Formula validation/trials require both metadata read and record read; workflow retry requires both metadata write and record write and explicit CLI confirmation (`--yes` in automation).
300
+
301
+ ### Reusable iframe configuration (4.11)
302
+
303
+ `flowbase iframes list|get|create|update|delete` manages environment-specific iframe drafts. Create/update use `--json-file`; fields are `label`, `icon` (default `IconWorld`), `color` (default `#2563EB`), and a complete HTTPS `url` without credentials. Update and delete require `baseRevision` from `iframes get`; deletion also requires confirmation (`--yes` for noninteractive use).
304
+
305
+ ```bash
306
+ flowbase iframes create --json-file iframe.json
307
+ flowbase iframes get ifr_example
308
+ flowbase iframes update ifr_example --json-file iframe-update.json
309
+ flowbase apps add-iframes app_example ifr_example
310
+ flowbase apps set-iframe-group app_example ifr_example --group-id grp_example
311
+ flowbase apps set-iframe-group app_example ifr_example --ungrouped
312
+ flowbase apps reorder-members app_example --json '{"items":[{"type":"iframe","id":"ifr_example"}]}'
313
+ flowbase apps remove-iframe app_example ifr_example --yes
314
+ flowbase iframes delete ifr_example --json '{"baseRevision":12}' --yes
315
+ ```
316
+
317
+ Removal only unlinks one app. Deletion unlinks all apps atomically. Changes follow the existing commit/release/rollback workflow; they do not fetch, proxy or modify the external website. API keys use `metadata:read`/`metadata:write`; browser roles use `iframe.manage` for shared resources and `app.manage` for app associations.
318
+
319
+ ### Custom object views (4.12)
320
+
321
+ Read `flowbase docs catalogs custom-views` and `flowbase views list OBJECT` first. Save the following as `view.json`, replacing `baseRevision` with the current `draftRevision`:
322
+
323
+ ```json
324
+ {
325
+ "baseRevision": 4,
326
+ "name": "Custom overview",
327
+ "enabled": false,
328
+ "defaultFieldIds": [],
329
+ "displayMode": "custom",
330
+ "custom": {
331
+ "runtimeVersion": "cv1",
332
+ "source": "import React from 'react'; export default function View(){return <div>Hello</div>}"
333
+ }
334
+ }
335
+ ```
336
+
337
+ ```bash
338
+ flowbase views create obj_example --json-file view.json
339
+ flowbase views list obj_example
340
+ flowbase views update obj_example viw_example --json-file view.json
341
+ flowbase views set-enabled obj_example viw_example --base-revision 6 --enabled --yes
342
+ ```
343
+
344
+ Use the revision returned by the preceding write, rather than the example numbers. Custom views start disabled; preview in the browser and enable when ready. Updates replace the writable snapshot: preserve `custom.source`, `custom.runtimeVersion`, `displayMode`, filters and other configuration from `views list`, excluding the response-only `id` and `status`. Built-in/custom view types cannot be converted. JSX source is limited to 512 KiB UTF-8; the runtime supports `react`, `three` and `flowbase`. CLI configuration does not execute browser previews.
345
+
346
+ ### Record detail embedded pages (4.13)
347
+
348
+ `flowbase docs catalogs page-components` documents `recordEmbed`, supported at the top level or inside tabs of a detail Page. Add this component to an existing region column (or a tab's `components`):
349
+
350
+ ```json
351
+ {
352
+ "kind": "recordEmbed",
353
+ "label": "External panel",
354
+ "showTitle": true,
355
+ "url": "https://example.com/panel",
356
+ "height": 480,
357
+ "allowRead": false,
358
+ "allowedFieldIds": []
359
+ }
360
+ ```
361
+
362
+ Read `flowbase pages get PAGE`, preserve the existing components and IDs, and remove the read-only Page keys listed in `docs catalogs page-components`. Wrap the editable snapshot as `{"baseRevision":4,"page":{...}}`, then use `flowbase pages update PAGE --json-file page.json` (or `pages create --json-file page.json` for a new detail Page). For creation, omit the Page ID and IDs of new regions/components so the server allocates them.
363
+
364
+ Height must be an integer from 240 to 1600. URLs must use external HTTPS and exclude credentials, identity/context parameters and templates. To permit SDK reads, set `allowRead: true` and select at least one unique active field ID in `allowedFieldIds`; current record/field permissions still apply. The browser SDK is `/sdk/record-embed.v1.js`. The CLI configures the component; rendering and SDK calls run in the browser. `flowbase-user` has no metadata management commands.
@@ -151,6 +151,14 @@ flowbase objects cli-edit-policy update obj_xxx --json '{"baseRevision":4,"polic
151
151
 
152
152
  `records create` returns `{recordId}`; use it with `records get`.
153
153
 
154
+ To add existing objects to an application, pass their IDs with `--object-ids`:
155
+
156
+ ```bash
157
+ flowbase apps add-objects app_xxx --object-ids obj_a,obj_b
158
+ ```
159
+
160
+ Use a comma-separated list for multiple objects. Object IDs are not positional arguments. Run `flowbase docs show apps.objects.add` for the endpoint details.
161
+
154
162
  Record `values` keys are **field keys** (`name`), not internal IDs (`fld_...`). IDs are `obj_`/`fld_`/`dsh_`/`wfl_`/`pag_`/`reg_`/`cmp_`-prefixed; see `docs catalogs id-prefixes`.
155
163
 
156
164
  ### 6.2 Edit and publish configuration on a test environment
@@ -271,3 +279,77 @@ flowbase roles update rol_xxx --json-file role.json
271
279
  - `docs catalogs page-buttons`, `workflow-branches`, `approval-rules`, and `dashboard-runtime` describe complex configuration and runtime limits.
272
280
 
273
281
  Transport requests time out after 60 seconds. A lost write response has an unknown outcome: read the resource and current revision before retrying. The CLI never automatically retries writes.
282
+
283
+ ### Recent feature authoring
284
+
285
+ - `flowbase docs catalogs planning`: tree/Gantt view snapshots, candidate queries and optimistic plan updates. Read a full view with `views list` before updating; preserve `displayMode`, `parentFieldId` and `gantt`.
286
+ - `flowbase docs catalogs record-bundles`: initialize, atomically submit and recover a parent/detail submission. Reuse a submission ID only with the identical request body.
287
+ - `flowbase docs catalogs formula-authoring`: formula/rollup sources, validation, saved-record trials and runtime draft preview. Derived values are read-only.
288
+ - `flowbase docs catalogs workflow-python`: record creation examples and failed-task retry. `workflows python-runtime OBJECT new` discovers current object bindings; `workflows executions` includes Python attempts, logs and `canRetry`.
289
+
290
+ Use `records list OBJECT --view-id VIEW --view-layout` for tree/Gantt projections; the default remains a flat list. `records update` accepts `updatedAt` and `planViewId` in its JSON body for version-checked plan edits. Formula validation/trials require both metadata read and record read; workflow retry requires both metadata write and record write and explicit CLI confirmation (`--yes` in automation).
291
+
292
+ ### Reusable iframe configuration (4.11)
293
+
294
+ `flowbase iframes list|get|create|update|delete` manages environment-specific iframe drafts. Create/update use `--json-file`; fields are `label`, `icon` (default `IconWorld`), `color` (default `#2563EB`), and a complete HTTPS `url` without credentials. Update and delete require `baseRevision` from `iframes get`; deletion also requires confirmation (`--yes` for noninteractive use).
295
+
296
+ ```bash
297
+ flowbase iframes create --json-file iframe.json
298
+ flowbase iframes get ifr_example
299
+ flowbase iframes update ifr_example --json-file iframe-update.json
300
+ flowbase apps add-iframes app_example ifr_example
301
+ flowbase apps set-iframe-group app_example ifr_example --group-id grp_example
302
+ flowbase apps set-iframe-group app_example ifr_example --ungrouped
303
+ flowbase apps reorder-members app_example --json '{"items":[{"type":"iframe","id":"ifr_example"}]}'
304
+ flowbase apps remove-iframe app_example ifr_example --yes
305
+ flowbase iframes delete ifr_example --json '{"baseRevision":12}' --yes
306
+ ```
307
+
308
+ Removal only unlinks one app. Deletion unlinks all apps atomically. Changes follow the existing commit/release/rollback workflow; they do not fetch, proxy or modify the external website. API keys use `metadata:read`/`metadata:write`; browser roles use `iframe.manage` for shared resources and `app.manage` for app associations.
309
+
310
+ ### Custom object views (4.12)
311
+
312
+ Read `flowbase docs catalogs custom-views` and `flowbase views list OBJECT` first. Save the following as `view.json`, replacing `baseRevision` with the current `draftRevision`:
313
+
314
+ ```json
315
+ {
316
+ "baseRevision": 4,
317
+ "name": "Custom overview",
318
+ "enabled": false,
319
+ "defaultFieldIds": [],
320
+ "displayMode": "custom",
321
+ "custom": {
322
+ "runtimeVersion": "cv1",
323
+ "source": "import React from 'react'; export default function View(){return <div>Hello</div>}"
324
+ }
325
+ }
326
+ ```
327
+
328
+ ```bash
329
+ flowbase views create obj_example --json-file view.json
330
+ flowbase views list obj_example
331
+ flowbase views update obj_example viw_example --json-file view.json
332
+ flowbase views set-enabled obj_example viw_example --base-revision 6 --enabled --yes
333
+ ```
334
+
335
+ Use the revision returned by the preceding write, rather than the example numbers. Custom views start disabled; preview in the browser and enable when ready. Updates replace the writable snapshot: preserve `custom.source`, `custom.runtimeVersion`, `displayMode`, filters and other configuration from `views list`, excluding the response-only `id` and `status`. Built-in/custom view types cannot be converted. JSX source is limited to 512 KiB UTF-8; the runtime supports `react`, `three` and `flowbase`. CLI configuration does not execute browser previews.
336
+
337
+ ### Record detail embedded pages (4.13)
338
+
339
+ `flowbase docs catalogs page-components` documents `recordEmbed`, supported at the top level or inside tabs of a detail Page. Add this component to an existing region column (or a tab's `components`):
340
+
341
+ ```json
342
+ {
343
+ "kind": "recordEmbed",
344
+ "label": "External panel",
345
+ "showTitle": true,
346
+ "url": "https://example.com/panel",
347
+ "height": 480,
348
+ "allowRead": false,
349
+ "allowedFieldIds": []
350
+ }
351
+ ```
352
+
353
+ Read `flowbase pages get PAGE`, preserve the existing components and IDs, and remove the read-only Page keys listed in `docs catalogs page-components`. Wrap the editable snapshot as `{"baseRevision":4,"page":{...}}`, then use `flowbase pages update PAGE --json-file page.json` (or `pages create --json-file page.json` for a new detail Page). For creation, omit the Page ID and IDs of new regions/components so the server allocates them.
354
+
355
+ Height must be an integer from 240 to 1600. URLs must use external HTTPS and exclude credentials, identity/context parameters and templates. To permit SDK reads, set `allowRead: true` and select at least one unique active field ID in `allowedFieldIds`; current record/field permissions still apply. The browser SDK is `/sdk/record-embed.v1.js`. The CLI configures the component; rendering and SDK calls run in the browser. `flowbase-user` has no metadata management commands.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "flowbase-cli"
7
- version = "0.1.14"
7
+ version = "0.1.16"
8
8
  description = "Command line client for the FlowBase OpenAPI"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,3 +1,3 @@
1
1
  """FlowBase OpenAPI CLI."""
2
2
 
3
- __version__ = "0.1.14"
3
+ __version__ = "0.1.16"
@@ -22,6 +22,53 @@ class Endpoint:
22
22
 
23
23
 
24
24
  ENDPOINTS = (
25
+ Endpoint("iframes.list", "flowbase_iframes_list", "GET", "/iframes", "metadata:read", "List iframe configuration.", "flowbase iframes list", requires_confirmation=False, request_body=None),
26
+ Endpoint("iframes.get", "flowbase_iframes_get", "GET", "/iframes/{iframe_id}", "metadata:read", "Get iframe configuration.", "flowbase iframes get", requires_confirmation=False, request_body=None),
27
+ Endpoint("iframes.create", "flowbase_iframes_create", "POST", "/iframes", "metadata:write", "Create iframe configuration.", "flowbase iframes create", requires_confirmation=False, request_body='{"label": "Help", "url": "https://example.com", "icon": "IconWorld", "color": "#2563EB"}'),
28
+ Endpoint("iframes.update", "flowbase_iframes_update", "PUT", "/iframes/{iframe_id}", "metadata:write", "Update iframe configuration.", "flowbase iframes update", requires_confirmation=False, request_body='{"label": "Help", "url": "https://example.com", "icon": "IconWorld", "color": "#2563EB", "baseRevision": 0}'),
29
+ Endpoint("iframes.delete", "flowbase_iframes_delete", "POST", "/iframes/{iframe_id}/delete", "metadata:write", "Delete iframe configuration.", "flowbase iframes delete", requires_confirmation=True, request_body='{"baseRevision": 0}'),
30
+ Endpoint(
31
+ "apps.iframes.add",
32
+ "flowbase_apps_iframes_add",
33
+ "POST",
34
+ "/apps/{app_id}/iframes",
35
+ "metadata:write",
36
+ "Add iframes to an app (association only).",
37
+ "flowbase apps add-iframes app_xxx ifr_xxx",
38
+ request_body='{"iframeIds":["ifr_xxx"]}',
39
+ ),
40
+ Endpoint(
41
+ "apps.iframes.remove",
42
+ "flowbase_apps_iframes_remove",
43
+ "DELETE",
44
+ "/apps/{app_id}/iframes/{iframe_id}",
45
+ "metadata:write",
46
+ "Remove a iframe association from an app.",
47
+ "flowbase apps remove-iframe app_xxx ifr_xxx --yes",
48
+ True,
49
+ ),
50
+ Endpoint(
51
+ "apps.iframes.set-group",
52
+ "flowbase_apps_iframes_set_group",
53
+ "POST",
54
+ "/apps/{app_id}/iframes/{iframe_id}/group",
55
+ "metadata:write",
56
+ "Move an app-linked iframe into a group or ungroup it.",
57
+ "flowbase apps set-iframe-group app_xxx ifr_xxx --group-id grp_xxx",
58
+ request_body='{"groupId":"grp_xxx"}',
59
+ ),
60
+
61
+ Endpoint('formulas.sources', 'flowbase_formulas_sources', 'GET', '/objects/{object_id}/formula/sources', 'metadata:read', 'Discover scalar formula sources in the draft.', 'flowbase formulas sources obj_xxx'),
62
+ Endpoint('formulas.collections', 'flowbase_formulas_collections', 'GET', '/objects/{object_id}/formula/collections', 'metadata:read', 'Discover collection object and field IDs in the draft.', 'flowbase formulas collections obj_xxx'),
63
+ Endpoint('formulas.validate', 'flowbase_formulas_validate', 'POST', '/objects/{object_id}/formula/validate', 'metadata:read', 'Validate formula or rollup; trial also supported. Requires records:read. No writes.', 'flowbase formulas validate obj_xxx --json-file formula.json', request_body='{"constraints":{"expression":"amount * 2","resultType":"number","scale":2},"samples":{"amount":12.50},"trial":true}'),
64
+ Endpoint('records.formula-preview', 'flowbase_records_formula_preview', 'POST', '/objects/{object_id}/formula-preview', 'records:read', 'Preview runtime formulas without saving. New rollups remain pending; saved rollups use saved data.', 'flowbase records formula-preview obj_xxx --json-file preview.json', request_body='{"values":{"amount":12.50}}'),
65
+ Endpoint('records.bundle-init', 'flowbase_records_bundle_init', 'GET', '/objects/{object_id}/bundle/init', 'records:read', 'Load configuration and collection versions before an atomic write.', 'flowbase records bundle-init obj_xxx --record-id rec_xxx'),
66
+ Endpoint('records.bundle-save', 'flowbase_records_bundle_save', 'POST', '/objects/{object_id}/bundle', 'records:write', 'Atomically save parent and configured related rows. Reuse submissionId only for identical retries.', 'flowbase records bundle-save obj_xxx --json-file bundle.json', request_body='{"submissionId":"unique-submission-0001","configVersion":"from-bundle-init","values":{"name":"Order"},"components":[]}'),
67
+ Endpoint('records.bundle-status', 'flowbase_records_bundle_status', 'GET', '/objects/{object_id}/bundle/submissions/{submission_id}', 'records:read', "Query this API key's submission receipt after a timeout; unknown does not prove failure.", 'flowbase records bundle-status obj_xxx unique-submission-0001'),
68
+ Endpoint('records.hierarchy-candidates', 'flowbase_records_hierarchy_candidates', 'GET', '/objects/{object_id}/hierarchy/candidates', 'records:read', 'Discover eligible parent records, excluding self and descendants.', 'flowbase records hierarchy-candidates obj_xxx --field fld_parent --exclude rec_xxx'),
69
+ Endpoint('records.gantt-candidates', 'flowbase_records_gantt_candidates', 'GET', '/objects/{object_id}/gantt/candidates', 'records:read', 'Discover eligible predecessors, excluding cycles and hierarchy conflicts.', 'flowbase records gantt-candidates obj_xxx --field fld_predecessors --exclude rec_xxx'),
70
+ Endpoint('workflows.python-runtime', 'flowbase_workflows_python_runtime', 'GET', '/objects/{object_id}/workflows/{workflow_id}/python-runtime', 'metadata:read', 'Discover Python APIs and creation methods. Use workflow ID new before creation.', 'flowbase workflows python-runtime obj_xxx new'),
71
+ Endpoint('workflows.retry', 'flowbase_workflows_retry', 'POST', '/objects/{object_id}/workflows/{workflow_id}/executions/{execution_id}/retry', 'metadata:write', 'Retry a failed Python execution; also requires records:write.', 'flowbase workflows retry obj_xxx wfl_xxx exe_xxx --yes', requires_confirmation=True),
25
72
  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
73
  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
74
  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"),
@@ -245,15 +292,15 @@ ENDPOINTS = (
245
292
  '"filters":['
246
293
  '{"fieldId":"fld_status","operator":"eq","value":"opt_xxx"},'
247
294
  '{"fieldId":"fld_done","operator":"is_true"}'
248
- '],"defaultFieldIds":["fld_a","fld_b"],"groupFieldId":null}'
295
+ '],"defaultFieldIds":["fld_a","fld_b"],"groupFieldId":null,"displayMode":"table","parentFieldId":null,"gantt":{}}'
249
296
  )),
250
- 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",
297
+ Endpoint("views.update", "flowbase_views_update", "PUT", "/objects/{object_id}/views/{view_id}", "metadata:write", "Update a complete view snapshot. Preserve displayMode, parentFieldId, gantt and custom from views.list; Read custom-views catalog for JSX configuration; omitted fields use defaults.", "flowbase views update obj_xxx viw_xxx --json-file view.json",
251
298
  request_body=(
252
299
  '{"baseRevision":4,"name":"Active","enabled":true,"match":"all",'
253
300
  '"filters":['
254
301
  '{"fieldId":"fld_status","operator":"eq","value":"opt_xxx"},'
255
302
  '{"fieldId":"fld_done","operator":"is_true"}'
256
- '],"defaultFieldIds":["fld_a","fld_b"],"groupFieldId":null}'
303
+ '],"defaultFieldIds":["fld_a","fld_b"],"groupFieldId":null,"displayMode":"table","parentFieldId":null,"gantt":{}}'
257
304
  )),
258
305
  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\"]}'",
259
306
  request_body='{"baseRevision":4,"viewIds":["viw_a","viw_b"]}'),
@@ -625,6 +672,17 @@ def _field_type(
625
672
 
626
673
 
627
674
  FIELD_TYPES = (
675
+ _field_type(
676
+ "rollup", ("source", "operation", "resultType", "scale", "currency", "unit"),
677
+ writable=False, record_value="derived scalar | null; omit on writes; see formulaStates",
678
+ extra={"permissions": ["readonly", "hidden"], "operations": ["sum", "avg", "count", "count_values", "min", "max"], "sourceModes": ["reverse", "forward", "children"]},
679
+ ),
680
+ _field_type(
681
+ "formula", ("expression", "resultType", "scale", "currency", "bindings", "languageVersion", "collections"),
682
+ writable=False, record_value="derived scalar | null; omit on writes; see formulaStates for failures",
683
+ extra={"permissions": ["readonly", "hidden"], "resultTypes": ["number", "number.currency", "number.percent", "text.singleLine", "date", "boolean"],
684
+ "createExample": {"label": "Total", "key": "total", "description": "Calculated total", "type": "formula", "typeVersion": "v1", "permission": "readonly", "constraints": {"expression": "quantity * unit_price", "resultType": "number", "scale": 2}}},
685
+ ),
628
686
  _field_type(
629
687
  "text.singleLine", ("maxLength", "minLength", "placeholder", "unique"),
630
688
  filter_operators=_TEXT_FILTERS, record_value="string | null",
@@ -1018,7 +1076,7 @@ APPROVAL_NODE_TYPES = (
1018
1076
  )
1019
1077
 
1020
1078
  FUNCTION_PERMISSIONS = (
1021
- "workspace.access", "app.manage", "object.manage", "dashboard.manage", "form.manage",
1079
+ "workspace.access", "app.manage", "object.manage", "dashboard.manage", "form.manage", "iframe.manage",
1022
1080
  "version.manage", "org.manage", "system.manage",
1023
1081
  )
1024
1082
 
@@ -1066,7 +1124,7 @@ PAGE_COMPONENTS = {
1066
1124
  ),
1067
1125
  "detailOnlyKinds": (
1068
1126
  "relatedList", "approval", "detailSummary", "stageBar", "tabs",
1069
- "relatedSummary", "attachmentCard", "noticeBlock",
1127
+ "relatedSummary", "attachmentCard", "noticeBlock", "recordEmbed",
1070
1128
  ),
1071
1129
  },
1072
1130
  "kinds": (
@@ -1099,7 +1157,8 @@ PAGE_COMPONENTS = {
1099
1157
  "kind", "label", "hideLabel", "objectId", "relationFieldId",
1100
1158
  "columnFieldIds", "sort", "limit", "showViewAll", "createAction",
1101
1159
  ),
1102
- "optionalKeys": ("id", "filters", "carryIns"),
1160
+ "optionalKeys": ("id", "filters", "carryIns", "editMode", "editOptions", "editColumns"),
1161
+ "editMode": {"withParent": "Atomic editing on create/detail pages; initialize with records bundle-init, then bundle-save."},
1103
1162
  "limit": (5, 10, 20),
1104
1163
  "sortDirections": ("asc", "desc"),
1105
1164
  "carryInSources": (
@@ -1184,7 +1243,7 @@ PAGE_COMPONENTS = {
1184
1243
  "minimumComponents": 1,
1185
1244
  "allowedChildKinds": (
1186
1245
  "field", "relatedList", "approval", "stageBar",
1187
- "relatedSummary", "attachmentCard", "noticeBlock",
1246
+ "relatedSummary", "attachmentCard", "noticeBlock", "recordEmbed",
1188
1247
  ),
1189
1248
  },
1190
1249
  "note": "defaultTabId must reference a tab in this component; tabs cannot be nested.",
@@ -1225,6 +1284,22 @@ PAGE_COMPONENTS = {
1225
1284
  "titleLengthWhenShown": (1, 50),
1226
1285
  },
1227
1286
  },
1287
+ {
1288
+ "kind": "recordEmbed",
1289
+ "name": "record detail embedded page",
1290
+ "pageTypes": ("detail",),
1291
+ "placement": ("topLevel", "tabs"),
1292
+ "requiredKeys": ("kind", "label", "url"),
1293
+ "optionalKeys": ("id", "showTitle", "height", "allowRead", "allowedFieldIds"),
1294
+ "defaults": {"showTitle": True, "height": 480, "allowRead": False, "allowedFieldIds": []},
1295
+ "constraints": {
1296
+ "labelLength": (1, 80),
1297
+ "height": "integer, 240–1600",
1298
+ "url": "External HTTPS only; no credentials, identity/context parameters, templates, localhost or FlowBase hosts.",
1299
+ "allowedFieldIds": "Unique active field IDs on the Page object; at least one when allowRead=true. Runtime reads also enforce current record and field permissions.",
1300
+ },
1301
+ "runtime": "Browser SDK /sdk/record-embed.v1.js; read-only bridge. CLI authors configuration and does not render or execute the embed.",
1302
+ },
1228
1303
  {
1229
1304
  "kind": "noticeBlock",
1230
1305
  "name": "notice block",
@@ -1300,7 +1375,7 @@ PAGE_RULES = {
1300
1375
  "states": ("visible", "hidden"),
1301
1376
  "targetKinds": (
1302
1377
  "relatedList", "approval", "detailSummary", "stageBar", "tabs",
1303
- "relatedSummary", "attachmentCard", "noticeBlock",
1378
+ "relatedSummary", "attachmentCard", "noticeBlock", "recordEmbed",
1304
1379
  ),
1305
1380
  "excludedTargets": ("field", "region", "tab", "button", "systemArea"),
1306
1381
  "sameRuleUniqueTarget": True,
@@ -1519,6 +1594,38 @@ FORMS = {
1519
1594
  }
1520
1595
 
1521
1596
  CATALOGS = {
1597
+ "custom-views": {
1598
+ "commands": "views list/create/update/set-enabled/delete; create/update accept --json-file.",
1599
+ "example": {"baseRevision": 4, "name": "Custom", "enabled": False, "defaultFieldIds": [], "displayMode": "custom", "custom": {"runtimeVersion": "cv1", "source": "import React from 'react'; export default function View(){return <div>Hello</div>}"}},
1600
+ "update": "Read views.list; preserve custom.source, custom.runtimeVersion, displayMode and the other writable snapshot fields. Use draftRevision as baseRevision; omit response-only id and status from the body.",
1601
+ "constraints": "Custom views start disabled; enable separately after validation. Built-in and custom views cannot convert into each other. custom contains exactly source and runtimeVersion=cv1; source must be nonempty and at most 512 KiB UTF-8.",
1602
+ "runtime": "Sandboxed browser JSX, with react/three/flowbase imports. CLI saves configuration; browser preview/read bridge is not exposed as an API-key CLI command.",
1603
+ },
1604
+ "planning": {
1605
+ "viewUpdate": "Read views.list, preserve the complete snapshot, then views.update with baseRevision. Omitting displayMode/parentFieldId/gantt resets them to table/null/{}.",
1606
+ "tree": {"displayMode": "tree", "parentFieldId": "fld_parent", "gantt": {}},
1607
+ "gantt": {"displayMode": "gantt", "parentFieldId": None, "gantt": {"startFieldId": "fld_start", "endFieldId": "fld_end", "predecessorFieldId": "fld_dependencies"}},
1608
+ "read": "records list OBJECT --view-id VIEW --view-layout; use --parent, --root, --locate and --hierarchy-version for tree navigation. Default is flat.",
1609
+ "write": {"values": {"start": "2026-10-24", "end": "2026-10-25"}, "planViewId": "viw_plan", "updatedAt": "from-records-get"},
1610
+ "candidates": "records hierarchy-candidates / gantt-candidates use field IDs. Record values use field keys.",
1611
+ },
1612
+ "record-bundles": {
1613
+ "steps": ["Configure relatedList editMode=withParent on the default create/detail page.", "records bundle-init OBJECT [--record-id RECORD] returns configVersion, updatedAt and component collection versions.", "records bundle-save OBJECT --json-file bundle.json commits the entire write set or rolls back.", "After a timeout, bundle-status OBJECT SUBMISSION; retry only the identical body with the same submissionId."],
1614
+ "example": {"submissionId": "unique-submission-0001", "configVersion": "from-init", "recordId": "rec_parent", "updatedAt": "from-init", "values": {}, "components": [{"componentId": "cmp_lines", "version": 1, "rows": [{"clientId": "line-1", "operation": "create", "values": {"quantity": 2}}]}]},
1615
+ "rules": "Copy every visible component and its version. Update/delete rows also require recordId and updatedAt. Relation is injected by the server; omit it from row values. API-key receipts are isolated per key. Conflict: reload, reconcile and use a new submissionId. Max 200 resulting detail rows.",
1616
+ },
1617
+ "formula-authoring": {
1618
+ "steps": ["formulas sources OBJECT discovers scalar paths; formulas collections OBJECT discovers stable object/field IDs.", "formulas validate OBJECT --json-file formula.json validates constraints; trial=true uses samples or a saved recordId.", "fields create/update saves metadata. records formula-preview previews runtime values without saving."],
1619
+ "rollupExample": {"fieldType": "rollup", "constraints": {"source": {"mode": "reverse", "objectId": "obj_lines", "relationFieldId": "fld_order", "fieldId": "fld_amount"}, "operation": "sum", "resultType": "number", "scale": 2}, "trial": True, "recordId": "rec_order"},
1620
+ "collectionExample": {"expression": "sum(lines)", "resultType": "number", "scale": 2, "languageVersion": 2, "collections": {"lines": {"mode": "reverse", "objectId": "obj_lines", "relationFieldId": "fld_order", "fieldId": "fld_amount"}}},
1621
+ "readOnly": "formula/rollup values cannot be written, filtered, sorted or SQL-aggregated through the employee CLI. Read results and formulaStates instead.",
1622
+ },
1623
+ "workflow-python": {
1624
+ "discover": "workflows python-runtime OBJECT new (or WORKFLOW) returns creationObjects, relation bindings and limits.",
1625
+ "example": 'import flowbase\n\ndef run():\n first = flowbase.records.create(object="task", values={"title": "Review"})\n others = flowbase.records.create_many(object="task", records=[{"title": "Prepare"}])\n flowbase.records.update(first, values={"title": "Review order"})\n',
1626
+ "atomicity": "Creates and updates commit together after successful execution. Use object/field keys, writable ordinary fields, and returned handles. At most 100 created+updated records.",
1627
+ "retry": "workflows executions OBJECT WORKFLOW lists results; workflows retry OBJECT WORKFLOW EXECUTION --yes retries eligible failed tasks using the original task contract.",
1628
+ },
1522
1629
  "workflow-branches": WORKFLOW_BRANCHES,
1523
1630
  "approval-rules": APPROVAL_RULES,
1524
1631
  "page-buttons": PAGE_BUTTONS,
@@ -467,6 +467,35 @@ def handle_apps(args: argparse.Namespace) -> int:
467
467
  path_params={"app_id": args.app_id, "dashboard_id": args.dashboard_id},
468
468
  body=body,
469
469
  )
470
+ if action == "add-iframes":
471
+ ids = [item.strip() for item in args.iframe_ids if item.strip()]
472
+ if not ids:
473
+ raise CliError("Provide at least one iframe ID.")
474
+ return run_request(
475
+ args,
476
+ "apps.iframes.add",
477
+ path_params={"app_id": args.app_id},
478
+ body={"iframeIds": ids},
479
+ )
480
+ if action == "remove-iframe":
481
+ return run_request(
482
+ args,
483
+ "apps.iframes.remove",
484
+ path_params={"app_id": args.app_id, "iframe_id": args.iframe_id},
485
+ )
486
+ if action == "set-iframe-group":
487
+ if args.ungrouped:
488
+ body = {"groupId": None}
489
+ elif args.group_id is not None:
490
+ body = {"groupId": args.group_id}
491
+ else:
492
+ raise CliError("Provide --group-id or --ungrouped.")
493
+ return run_request(
494
+ args,
495
+ "apps.iframes.set-group",
496
+ path_params={"app_id": args.app_id, "iframe_id": args.iframe_id},
497
+ body=body,
498
+ )
470
499
  if action == "reorder-members":
471
500
  body = _payload_from_args(args, {}, ("items",))
472
501
  if not isinstance(body["items"], list):
@@ -522,6 +551,13 @@ def handle_dashboards(args: argparse.Namespace) -> int:
522
551
  )
523
552
 
524
553
 
554
+ def handle_iframes(args: argparse.Namespace) -> int:
555
+ action = args.iframes_action
556
+ params = {"iframe_id": args.iframe_id} if action in ("get", "update", "delete") else None
557
+ body = load_json_payload(args.json, args.json_file) if action in ("create", "update", "delete") else None
558
+ return run_request(args, f"iframes.{action}", path_params=params, body=body)
559
+
560
+
525
561
  def handle_forms(args: argparse.Namespace) -> int:
526
562
  action = args.forms_action
527
563
  if action == "list":
@@ -681,6 +717,10 @@ def handle_workflows(args: argparse.Namespace) -> int:
681
717
  path = {"object_id": args.object_id}
682
718
  if hasattr(args, "workflow_id"):
683
719
  path["workflow_id"] = args.workflow_id
720
+ if action in ("python-runtime", "retry"):
721
+ if action == "retry":
722
+ path["execution_id"] = args.execution_id
723
+ return run_request(args, f"workflows.{action}", path_params=path)
684
724
  if action == "list":
685
725
  return run_request(args, "workflows.list", path_params=path)
686
726
  if action == "get":
@@ -941,8 +981,26 @@ def handle_environments(args: argparse.Namespace) -> int:
941
981
  return 0
942
982
 
943
983
 
984
+ def handle_formulas(args: argparse.Namespace) -> int:
985
+ body = load_json_payload(args.json, args.json_file) if args.formulas_action == "validate" else None
986
+ return run_request(args, f"formulas.{args.formulas_action}", path_params={"object_id": args.object_id}, body=body)
987
+
988
+
944
989
  def handle_records(args: argparse.Namespace) -> int:
945
990
  action = args.records_action
991
+ if action in ("bundle-init", "bundle-status", "bundle-save", "formula-preview", "hierarchy-candidates", "gantt-candidates"):
992
+ path = {"object_id": args.object_id}
993
+ query = {}
994
+ body = None
995
+ if action == "bundle-init":
996
+ query = {"record_id": args.record_id}
997
+ elif action == "bundle-status":
998
+ path["submission_id"] = args.submission_id
999
+ elif action in ("bundle-save", "formula-preview"):
1000
+ body = load_json_payload(args.json, args.json_file)
1001
+ else:
1002
+ query = {"field": args.field, "exclude": args.exclude, "q": args.query}
1003
+ return run_request(args, f"records.{action}", path_params=path, query=query, body=body)
946
1004
  path = {"object_id": args.object_id}
947
1005
  if action == "upload-attachment":
948
1006
  body, content_type = multipart_upload(args.file)
@@ -972,7 +1030,7 @@ def handle_records(args: argparse.Namespace) -> int:
972
1030
  if hasattr(args, "record_id"):
973
1031
  path["record_id"] = args.record_id
974
1032
  if action == "list":
975
- return run_request(args, "records.list", path_params=path, query={"page": args.page, "viewId": args.view_id})
1033
+ return run_request(args, "records.list", path_params=path, query={"page": args.page, "viewId": args.view_id, **({"flat": "false"} if args.view_layout else {}), **{key: value for key, value in {"root": args.root, "parent": args.parent, "locate": args.locate, "hierarchyVersion": args.hierarchy_version}.items() if value is not None}})
976
1034
  if action == "get" or action == "delete":
977
1035
  return run_request(args, f"records.{action}", path_params=path)
978
1036
  body = load_json_payload(args.json, args.json_file)
@@ -1140,6 +1198,26 @@ def build_parser() -> argparse.ArgumentParser:
1140
1198
  action="store_true",
1141
1199
  help="Remove the dashboard from its current group in this app.",
1142
1200
  )
1201
+ app_add_iframes = app_actions.add_parser("add-iframes")
1202
+ app_add_iframes.add_argument("app_id")
1203
+ app_add_iframes.add_argument(
1204
+ "iframe_ids",
1205
+ nargs="+",
1206
+ help="One or more iframe IDs to associate with the app.",
1207
+ )
1208
+ app_remove_iframe = app_actions.add_parser("remove-iframe")
1209
+ app_remove_iframe.add_argument("app_id")
1210
+ app_remove_iframe.add_argument("iframe_id")
1211
+ add_yes(app_remove_iframe)
1212
+ app_set_iframe_group = app_actions.add_parser("set-iframe-group")
1213
+ app_set_iframe_group.add_argument("app_id")
1214
+ app_set_iframe_group.add_argument("iframe_id")
1215
+ app_set_iframe_group.add_argument("--group-id", help="Target group ID. Omit with --ungrouped.")
1216
+ app_set_iframe_group.add_argument(
1217
+ "--ungrouped",
1218
+ action="store_true",
1219
+ help="Remove the iframe from its current group in this app.",
1220
+ )
1143
1221
  app_reorder_members = app_actions.add_parser("reorder-members")
1144
1222
  app_reorder_members.add_argument("app_id")
1145
1223
  add_json_args(app_reorder_members, endpoint_key="apps.members.reorder")
@@ -1170,6 +1248,18 @@ def build_parser() -> argparse.ArgumentParser:
1170
1248
  add_yes(dashboard_delete)
1171
1249
  dashboards.set_defaults(handler=handle_dashboards)
1172
1250
 
1251
+ iframes = groups.add_parser("iframes", help="Manage reusable iframe configurations.")
1252
+ iframe_actions = iframes.add_subparsers(dest="iframes_action", required=True)
1253
+ for action in ("list", "get", "create", "update", "delete"):
1254
+ command = iframe_actions.add_parser(action)
1255
+ if action in ("get", "update", "delete"):
1256
+ command.add_argument("iframe_id")
1257
+ if action in ("create", "update", "delete"):
1258
+ add_json_args(command, endpoint_key=f"iframes.{action}")
1259
+ if action == "delete":
1260
+ add_yes(command)
1261
+ iframes.set_defaults(handler=handle_iframes)
1262
+
1173
1263
  forms = groups.add_parser("forms", help="Manage custom forms (public collection links).")
1174
1264
  form_actions = forms.add_subparsers(dest="forms_action", required=True)
1175
1265
  form_actions.add_parser("list")
@@ -1336,6 +1426,14 @@ def build_parser() -> argparse.ArgumentParser:
1336
1426
  workflow_executions.add_argument("object_id")
1337
1427
  workflow_executions.add_argument("workflow_id")
1338
1428
  workflow_executions.add_argument("--result", choices=("success", "failure"))
1429
+ runtime_help = workflow_actions.add_parser("python-runtime")
1430
+ runtime_help.add_argument("object_id")
1431
+ runtime_help.add_argument("workflow_id", help="Use new for a workflow not yet created.")
1432
+ retry = workflow_actions.add_parser("retry")
1433
+ retry.add_argument("object_id")
1434
+ retry.add_argument("workflow_id")
1435
+ retry.add_argument("execution_id")
1436
+ add_yes(retry)
1339
1437
  workflows.set_defaults(handler=handle_workflows)
1340
1438
 
1341
1439
  approvals = groups.add_parser("approvals", help="Manage object approval flow metadata.")
@@ -1473,6 +1571,11 @@ def build_parser() -> argparse.ArgumentParser:
1473
1571
  record_list = record_actions.add_parser("list")
1474
1572
  record_list.add_argument("object_id")
1475
1573
  record_list.add_argument("--page", type=int, default=1)
1574
+ record_list.add_argument("--view-layout", action="store_true", help="Return tree/Gantt projection instead of the default flat list.")
1575
+ record_list.add_argument("--root")
1576
+ record_list.add_argument("--parent")
1577
+ record_list.add_argument("--locate")
1578
+ record_list.add_argument("--hierarchy-version")
1476
1579
  record_list.add_argument("--view-id", help="Select a view returned by views list.")
1477
1580
  record_get = record_actions.add_parser("get")
1478
1581
  record_get.add_argument("object_id")
@@ -1497,6 +1600,19 @@ def build_parser() -> argparse.ArgumentParser:
1497
1600
  record_delete.add_argument("object_id")
1498
1601
  record_delete.add_argument("record_id")
1499
1602
  add_yes(record_delete)
1603
+ for action in ("bundle-init", "bundle-status", "bundle-save", "formula-preview", "hierarchy-candidates", "gantt-candidates"):
1604
+ item = record_actions.add_parser(action)
1605
+ item.add_argument("object_id")
1606
+ if action == "bundle-init":
1607
+ item.add_argument("--record-id")
1608
+ elif action == "bundle-status":
1609
+ item.add_argument("submission_id")
1610
+ elif action in ("bundle-save", "formula-preview"):
1611
+ add_json_args(item, endpoint_key=f"records.{action}")
1612
+ else:
1613
+ item.add_argument("--field", required=True, help="Stable field ID.")
1614
+ item.add_argument("--exclude", required=action == "gantt-candidates", default="")
1615
+ item.add_argument("--query", default="")
1500
1616
  records.set_defaults(handler=handle_records)
1501
1617
 
1502
1618
  roles = groups.add_parser("roles", help="Manage organization roles (production Host only).")
@@ -1516,6 +1632,14 @@ def build_parser() -> argparse.ArgumentParser:
1516
1632
  role_delete.add_argument("role_id")
1517
1633
  add_yes(role_delete)
1518
1634
  roles.set_defaults(handler=handle_roles)
1635
+ formulas = groups.add_parser("formulas", help="Discover, validate and trial formula/rollup fields.")
1636
+ formula_actions = formulas.add_subparsers(dest="formulas_action", required=True)
1637
+ for action in ("sources", "collections", "validate"):
1638
+ item = formula_actions.add_parser(action)
1639
+ item.add_argument("object_id")
1640
+ if action == "validate":
1641
+ add_json_args(item, endpoint_key="formulas.validate")
1642
+ formulas.set_defaults(handler=handle_formulas)
1519
1643
  return parser
1520
1644
 
1521
1645
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flowbase-cli
3
- Version: 0.1.14
3
+ Version: 0.1.16
4
4
  Summary: Command line client for the FlowBase OpenAPI
5
5
  Author: DeepFlowAI
6
6
  License: Apache-2.0
@@ -160,6 +160,14 @@ flowbase objects cli-edit-policy update obj_xxx --json '{"baseRevision":4,"polic
160
160
 
161
161
  `records create` returns `{recordId}`; use it with `records get`.
162
162
 
163
+ To add existing objects to an application, pass their IDs with `--object-ids`:
164
+
165
+ ```bash
166
+ flowbase apps add-objects app_xxx --object-ids obj_a,obj_b
167
+ ```
168
+
169
+ Use a comma-separated list for multiple objects. Object IDs are not positional arguments. Run `flowbase docs show apps.objects.add` for the endpoint details.
170
+
163
171
  Record `values` keys are **field keys** (`name`), not internal IDs (`fld_...`). IDs are `obj_`/`fld_`/`dsh_`/`wfl_`/`pag_`/`reg_`/`cmp_`-prefixed; see `docs catalogs id-prefixes`.
164
172
 
165
173
  ### 6.2 Edit and publish configuration on a test environment
@@ -280,3 +288,77 @@ flowbase roles update rol_xxx --json-file role.json
280
288
  - `docs catalogs page-buttons`, `workflow-branches`, `approval-rules`, and `dashboard-runtime` describe complex configuration and runtime limits.
281
289
 
282
290
  Transport requests time out after 60 seconds. A lost write response has an unknown outcome: read the resource and current revision before retrying. The CLI never automatically retries writes.
291
+
292
+ ### Recent feature authoring
293
+
294
+ - `flowbase docs catalogs planning`: tree/Gantt view snapshots, candidate queries and optimistic plan updates. Read a full view with `views list` before updating; preserve `displayMode`, `parentFieldId` and `gantt`.
295
+ - `flowbase docs catalogs record-bundles`: initialize, atomically submit and recover a parent/detail submission. Reuse a submission ID only with the identical request body.
296
+ - `flowbase docs catalogs formula-authoring`: formula/rollup sources, validation, saved-record trials and runtime draft preview. Derived values are read-only.
297
+ - `flowbase docs catalogs workflow-python`: record creation examples and failed-task retry. `workflows python-runtime OBJECT new` discovers current object bindings; `workflows executions` includes Python attempts, logs and `canRetry`.
298
+
299
+ Use `records list OBJECT --view-id VIEW --view-layout` for tree/Gantt projections; the default remains a flat list. `records update` accepts `updatedAt` and `planViewId` in its JSON body for version-checked plan edits. Formula validation/trials require both metadata read and record read; workflow retry requires both metadata write and record write and explicit CLI confirmation (`--yes` in automation).
300
+
301
+ ### Reusable iframe configuration (4.11)
302
+
303
+ `flowbase iframes list|get|create|update|delete` manages environment-specific iframe drafts. Create/update use `--json-file`; fields are `label`, `icon` (default `IconWorld`), `color` (default `#2563EB`), and a complete HTTPS `url` without credentials. Update and delete require `baseRevision` from `iframes get`; deletion also requires confirmation (`--yes` for noninteractive use).
304
+
305
+ ```bash
306
+ flowbase iframes create --json-file iframe.json
307
+ flowbase iframes get ifr_example
308
+ flowbase iframes update ifr_example --json-file iframe-update.json
309
+ flowbase apps add-iframes app_example ifr_example
310
+ flowbase apps set-iframe-group app_example ifr_example --group-id grp_example
311
+ flowbase apps set-iframe-group app_example ifr_example --ungrouped
312
+ flowbase apps reorder-members app_example --json '{"items":[{"type":"iframe","id":"ifr_example"}]}'
313
+ flowbase apps remove-iframe app_example ifr_example --yes
314
+ flowbase iframes delete ifr_example --json '{"baseRevision":12}' --yes
315
+ ```
316
+
317
+ Removal only unlinks one app. Deletion unlinks all apps atomically. Changes follow the existing commit/release/rollback workflow; they do not fetch, proxy or modify the external website. API keys use `metadata:read`/`metadata:write`; browser roles use `iframe.manage` for shared resources and `app.manage` for app associations.
318
+
319
+ ### Custom object views (4.12)
320
+
321
+ Read `flowbase docs catalogs custom-views` and `flowbase views list OBJECT` first. Save the following as `view.json`, replacing `baseRevision` with the current `draftRevision`:
322
+
323
+ ```json
324
+ {
325
+ "baseRevision": 4,
326
+ "name": "Custom overview",
327
+ "enabled": false,
328
+ "defaultFieldIds": [],
329
+ "displayMode": "custom",
330
+ "custom": {
331
+ "runtimeVersion": "cv1",
332
+ "source": "import React from 'react'; export default function View(){return <div>Hello</div>}"
333
+ }
334
+ }
335
+ ```
336
+
337
+ ```bash
338
+ flowbase views create obj_example --json-file view.json
339
+ flowbase views list obj_example
340
+ flowbase views update obj_example viw_example --json-file view.json
341
+ flowbase views set-enabled obj_example viw_example --base-revision 6 --enabled --yes
342
+ ```
343
+
344
+ Use the revision returned by the preceding write, rather than the example numbers. Custom views start disabled; preview in the browser and enable when ready. Updates replace the writable snapshot: preserve `custom.source`, `custom.runtimeVersion`, `displayMode`, filters and other configuration from `views list`, excluding the response-only `id` and `status`. Built-in/custom view types cannot be converted. JSX source is limited to 512 KiB UTF-8; the runtime supports `react`, `three` and `flowbase`. CLI configuration does not execute browser previews.
345
+
346
+ ### Record detail embedded pages (4.13)
347
+
348
+ `flowbase docs catalogs page-components` documents `recordEmbed`, supported at the top level or inside tabs of a detail Page. Add this component to an existing region column (or a tab's `components`):
349
+
350
+ ```json
351
+ {
352
+ "kind": "recordEmbed",
353
+ "label": "External panel",
354
+ "showTitle": true,
355
+ "url": "https://example.com/panel",
356
+ "height": 480,
357
+ "allowRead": false,
358
+ "allowedFieldIds": []
359
+ }
360
+ ```
361
+
362
+ Read `flowbase pages get PAGE`, preserve the existing components and IDs, and remove the read-only Page keys listed in `docs catalogs page-components`. Wrap the editable snapshot as `{"baseRevision":4,"page":{...}}`, then use `flowbase pages update PAGE --json-file page.json` (or `pages create --json-file page.json` for a new detail Page). For creation, omit the Page ID and IDs of new regions/components so the server allocates them.
363
+
364
+ Height must be an integer from 240 to 1600. URLs must use external HTTPS and exclude credentials, identity/context parameters and templates. To permit SDK reads, set `allowRead: true` and select at least one unique active field ID in `allowedFieldIds`; current record/field permissions still apply. The browser SDK is `/sdk/record-embed.v1.js`. The CLI configures the component; rendering and SDK calls run in the browser. `flowbase-user` has no metadata management commands.
@@ -10,4 +10,6 @@ src/flowbase_cli.egg-info/dependency_links.txt
10
10
  src/flowbase_cli.egg-info/entry_points.txt
11
11
  src/flowbase_cli.egg-info/top_level.txt
12
12
  tests/test_autonomy.py
13
- tests/test_cli.py
13
+ tests/test_cli.py
14
+ tests/test_iframes.py
15
+ tests/test_recent_commands.py
@@ -191,7 +191,7 @@ class FlowBaseCliTest(unittest.TestCase):
191
191
  self.assertEqual(code, 0)
192
192
  field_types = json.loads(stdout.getvalue())
193
193
  type_names = {item["type"] for item in field_types}
194
- self.assertEqual(len(field_types), 25)
194
+ self.assertEqual(len(field_types), 27)
195
195
  self.assertIn("select.single", type_names)
196
196
  self.assertIn("lookup.employee", type_names)
197
197
  self.assertTrue({
@@ -261,7 +261,7 @@ class FlowBaseCliTest(unittest.TestCase):
261
261
  kinds = {item.get("kind") for item in page_components["kinds"]}
262
262
  self.assertEqual(kinds, {
263
263
  None, "relatedList", "approval", "detailSummary", "stageBar",
264
- "tabs", "relatedSummary", "attachmentCard", "noticeBlock",
264
+ "tabs", "relatedSummary", "attachmentCard", "noticeBlock", "recordEmbed",
265
265
  })
266
266
  approval = next(
267
267
  item for item in page_components["kinds"] if item.get("kind") == "approval"
@@ -0,0 +1,30 @@
1
+ """Iframe CLI uses the same API contracts as browser management."""
2
+ import io
3
+ from unittest.mock import patch
4
+ import pytest
5
+ from flowbase_cli.main import main
6
+
7
+
8
+ @pytest.mark.parametrize("args,endpoint,path,body", [
9
+ (["iframes", "list"], "iframes.list", None, None),
10
+ (["iframes", "get", "ifr_x"], "iframes.get", {"iframe_id": "ifr_x"}, None),
11
+ (["iframes", "create", "--json", '{"label":"Help","url":"https://example.com"}'], "iframes.create", None, {"label":"Help","url":"https://example.com"}),
12
+ (["iframes", "update", "ifr_x", "--json", '{"label":"Help","url":"https://example.com","baseRevision":3}'], "iframes.update", {"iframe_id":"ifr_x"}, {"label":"Help","url":"https://example.com","baseRevision":3}),
13
+ (["iframes", "delete", "ifr_x", "--yes", "--json", '{"baseRevision":4}'], "iframes.delete", {"iframe_id":"ifr_x"}, {"baseRevision":4}),
14
+ (["apps", "add-iframes", "app_x", "ifr_x", "ifr_y"], "apps.iframes.add", {"app_id":"app_x"}, {"iframeIds":["ifr_x","ifr_y"]}),
15
+ (["apps", "remove-iframe", "app_x", "ifr_x", "--yes"], "apps.iframes.remove", {"app_id":"app_x","iframe_id":"ifr_x"}, None),
16
+ (["apps", "set-iframe-group", "app_x", "ifr_x", "--ungrouped"], "apps.iframes.set-group", {"app_id":"app_x","iframe_id":"ifr_x"}, {"groupId":None}),
17
+ (["apps", "reorder-members", "app_x", "--json", '{"items":[{"type":"iframe","id":"ifr_x"}]}'], "apps.members.reorder", {"app_id":"app_x"}, {"items":[{"type":"iframe","id":"ifr_x"}]}),
18
+ ])
19
+ def test_iframe_cli_request(args, endpoint, path, body):
20
+ with patch("flowbase_cli.main.OpenApiClient.request", return_value=b"{}") as request, patch("sys.stdout", io.StringIO()):
21
+ assert main(args) == 0
22
+ assert request.call_args.args[0] == endpoint
23
+ assert request.call_args.kwargs["path_params"] == path
24
+ assert request.call_args.kwargs["body"] == body
25
+
26
+
27
+ def test_delete_requires_confirmation():
28
+ with patch("flowbase_cli.main.OpenApiClient.request") as request, patch("sys.stdin.isatty", return_value=False), patch("sys.stderr", io.StringIO()):
29
+ assert main(["iframes", "delete", "ifr_x", "--json", '{"baseRevision":0}']) == 1
30
+ request.assert_not_called()
@@ -0,0 +1,30 @@
1
+ """New discovery commands and retry confirmation remain usable without a browser."""
2
+ import io
3
+ from unittest.mock import patch
4
+
5
+ import pytest
6
+
7
+ from flowbase_cli.main import main
8
+
9
+
10
+ @pytest.mark.parametrize('args,endpoint,path,query', [
11
+ (['formulas', 'sources', 'obj_x'], 'formulas.sources', {'object_id': 'obj_x'}, None),
12
+ (['formulas', 'collections', 'obj_x'], 'formulas.collections', {'object_id': 'obj_x'}, None),
13
+ (['records', 'bundle-init', 'obj_x', '--record-id', 'rec_x'], 'records.bundle-init', {'object_id': 'obj_x'}, {'record_id': 'rec_x'}),
14
+ (['records', 'bundle-status', 'obj_x', 'submission'], 'records.bundle-status', {'object_id': 'obj_x', 'submission_id': 'submission'}, {}),
15
+ (['records', 'hierarchy-candidates', 'obj_x', '--field', 'fld_x'], 'records.hierarchy-candidates', {'object_id': 'obj_x'}, {'field': 'fld_x', 'exclude': '', 'q': ''}),
16
+ (['records', 'gantt-candidates', 'obj_x', '--field', 'fld_x', '--exclude', 'rec_x'], 'records.gantt-candidates', {'object_id': 'obj_x'}, {'field': 'fld_x', 'exclude': 'rec_x', 'q': ''}),
17
+ (['workflows', 'python-runtime', 'obj_x', 'new'], 'workflows.python-runtime', {'object_id': 'obj_x', 'workflow_id': 'new'}, None),
18
+ ])
19
+ def test_discovery_request(args, endpoint, path, query):
20
+ with patch('flowbase_cli.main.OpenApiClient.request', return_value=b'{}') as request, patch('sys.stdout', io.StringIO()):
21
+ assert main(args) == 0
22
+ assert request.call_args.args[0] == endpoint
23
+ assert request.call_args.kwargs['path_params'] == path
24
+ assert request.call_args.kwargs['query'] == query
25
+
26
+
27
+ def test_retry_noninteractive_requires_explicit_confirmation():
28
+ with patch('flowbase_cli.main.OpenApiClient.request') as request, patch('sys.stdin.isatty', return_value=False), patch('sys.stderr', io.StringIO()):
29
+ assert main(['workflows', 'retry', 'obj_x', 'wfl_x', 'exe_x']) == 1
30
+ request.assert_not_called()
File without changes