softr-vibe-coding 2.11.1 → 2.13.0
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.
- package/CHANGELOG.md +9 -0
- package/README.md +10 -2
- package/SKILL.md +14 -5
- package/datasources/hubspot.md +392 -39
- package/datasources/multi-datasource.md +15 -6
- package/datasources/overview.md +1 -1
- package/datasources/reading.md +7 -0
- package/datasources/writing.md +7 -0
- package/package.json +1 -1
- package/references/browser-checks.md +199 -0
- package/references/editable-settings.md +1 -1
- package/references/softr-mcp.md +83 -7
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,15 @@ All notable changes to this skill are documented here. Versions follow [Semantic
|
|
|
4
4
|
|
|
5
5
|
Entries from 1.3.1 onward are generated automatically from git commit subjects between version bumps (see `.github/workflows/publish.yml`). Entries before 1.3.1 were backfilled by hand from the existing commit history.
|
|
6
6
|
|
|
7
|
+
## [2.13.0] - 2026-10-05
|
|
8
|
+
- Release 2.13.0
|
|
9
|
+
- Record the USER::: user-field Source-condition token (verified 2026-10-05 on HubSpot)
|
|
10
|
+
- Block Visibility is enforced server-side on the records endpoints (verified 2026-10-05)
|
|
11
|
+
- Rewrite HubSpot guidance from the 2026-10-05 verification run
|
|
12
|
+
|
|
13
|
+
## [2.12.0] - 2026-10-01
|
|
14
|
+
- Add references/browser-checks.md: checking a pushed block in a browser with agent-browser (verified 2026-10-01)
|
|
15
|
+
|
|
7
16
|
## [2.11.1] - 2026-10-01
|
|
8
17
|
- Correct four 2.11.0 statements after an independent fact-check of the transcripts
|
|
9
18
|
|
package/README.md
CHANGED
|
@@ -197,6 +197,12 @@ softr-vibe-coding/
|
|
|
197
197
|
│ │ # (Sep 18 2026); Oct 1 2026: tool rename map,
|
|
198
198
|
│ │ # push verification by sourceSha256, stub tools
|
|
199
199
|
│ │ # after a resume, update_field/update_table fixes
|
|
200
|
+
│ ├── browser-checks.md # Checking a pushed block in a browser with
|
|
201
|
+
│ │ # the agent-browser CLI (ask before installing):
|
|
202
|
+
│ │ # preview cookie, shadow-DOM refs grepped in the
|
|
203
|
+
│ │ # shell, eval measurements, the records-trigger
|
|
204
|
+
│ │ # write guard proven before any click, what a
|
|
205
|
+
│ │ # click sent, screenshots to disk (Oct 1 2026)
|
|
200
206
|
│ ├── advanced-integrations.md # Shadow DOM CSS isolation
|
|
201
207
|
│ │ # Leaflet, Mapbox, TinyMCE, Quill, FullCalendar
|
|
202
208
|
│ ├── native-chrome-styling.md # Restyle Softr's native shell (header, footer,
|
|
@@ -257,7 +263,8 @@ softr-vibe-coding/
|
|
|
257
263
|
│ # the from: parameter, getting the datasource UUIDs,
|
|
258
264
|
│ # select: as a module-scope identifier, the union-of-
|
|
259
265
|
│ # selects read payload (a conditional select is not
|
|
260
|
-
│ # privacy), Actions per table (Sep 18 2026)
|
|
266
|
+
│ # privacy), Actions per table (Sep 18 2026);
|
|
267
|
+
│ # block Visibility gates its endpoints (Oct 5 2026)
|
|
261
268
|
├── reading.md # useRecords, filtering, sorting, pagination,
|
|
262
269
|
│ # metrics, charts, current user; no detail-page
|
|
263
270
|
│ # auto-scoping, useRecords ignores enabled:false,
|
|
@@ -270,7 +277,8 @@ softr-vibe-coding/
|
|
|
270
277
|
├── softr-database.md # Native DB — field IDs, no rate limits
|
|
271
278
|
├── airtable.md # Column names, PAT vs OAuth, rate limits
|
|
272
279
|
├── google-sheets.md # Text formatting, 50-100 user cap
|
|
273
|
-
├── hubspot.md #
|
|
280
|
+
├── hubspot.md # 15 objects (listed ≠ usable), field model,
|
|
281
|
+
│ # association writes (open), row scoping (Oct 5 2026)
|
|
274
282
|
├── notion.md # Database pages only, Relation workarounds
|
|
275
283
|
├── coda.md # API token auth, limitations
|
|
276
284
|
├── monday.md # API token, Connected Boards
|
package/SKILL.md
CHANGED
|
@@ -96,6 +96,7 @@ You generate complete, production-ready Softr Vibe Coding blocks as TypeScript R
|
|
|
96
96
|
- Array-setting rows keyed by **index**, never by a builder-editable field value
|
|
97
97
|
- Media settings that may start empty (`src: ""`) gated with a conditional render or placeholder — never an unconditional `<img src={setting.src}>`
|
|
98
98
|
- **Deploying through the MCP:** `errors: null` on a push is not proof — compare the push result's `sourceSha256` with `shasum -a 256` of the file you sent, every byte counted, trailing newline included (Softr stores exactly what it receives; the one-byte drift we once blamed on it was a chunked read on our side). Prove deployed == disk *before* editing the same way, with `vibe_coding_block_get_code` and `includeCode: false`, so a Studio-side change is never overwritten. Fetch the full `sourceCode` only when a digest is missing or the hashes differ. Protocol in [references/softr-mcp.md → Verifying a push](references/softr-mcp.md#verifying-a-push--the-deployed-source-is-the-only-proof)
|
|
99
|
+
- **Then check it in a browser.** Once the push checks pass, check rendering and behaviour in a fresh preview with saves blocked, per [references/browser-checks.md](references/browser-checks.md)
|
|
99
100
|
|
|
100
101
|
## What to Clarify
|
|
101
102
|
|
|
@@ -180,6 +181,7 @@ For advanced patterns beyond data fetching, load the relevant reference when the
|
|
|
180
181
|
| Small reusable patterns — `localStorage` cross-page state, clipboard copy button | [references/common-patterns.md](references/common-patterns.md) |
|
|
181
182
|
| Writing Airtable Automation Scripts / Scripting Extension scripts / Airtable formulas — companion to Softr blocks for cross-table cascades and computed values | [references/airtable-automations.md](references/airtable-automations.md) |
|
|
182
183
|
| The official **Softr MCP server** — Softr DB schema + full record/table/field/database CRUD (deletes included), field-level browsing of connected integrations (Airtable / Google Sheets / Notion / Supabase and more), **creating, editing, versioning, and deploying Vibe Coding blocks directly** (`vibe_coding_block_get_docs`, `vibe_coding_block_create`, ...), push verification by `sourceSha256`, app management/scaffolding, the **Softr Workflows** suite (28 tools, 418-node catalog), and **per-application MCP servers**. Tool names changed on 2026-10-01; the file carries the old → new map | [references/softr-mcp.md](references/softr-mcp.md) |
|
|
184
|
+
| **Checking a pushed block in a browser** — rendering and behaviour in a Softr preview with the agent-browser CLI (ask before installing it): the preview cookie, reaching into the block's shadow DOM through accessibility refs, measuring with `eval`, blocking and proving the save endpoint before any click, reading what a click sent, screenshots to disk | [references/browser-checks.md](references/browser-checks.md) |
|
|
183
185
|
| Restyling Softr's **native shell — header / footer / nav / dropdowns / page background** (not a block; it's Softr chrome, done with global Custom Code CSS): stable selectors vs. hashed classes, floating "island" header+footer, the dropdown blank-space grid fix, the multi-layer page-background stacking, restyle-vs-replace | [references/native-chrome-styling.md](references/native-chrome-styling.md) |
|
|
184
186
|
| Adding a **dynamic date filter or custom filter control to a native List/Grid block** (via a Custom Code Static block, not a Vibe block): drive the block's conditional filter with `{URL_PARAM:…}`, the empty-param "match nothing" wide-range sentinel, inject the control into the filter row and keep it alive across Softr's re-renders | [references/native-block-filters.md](references/native-block-filters.md) |
|
|
185
187
|
| **Editable settings deep-dive** — full hook catalog (incl. verified-undocumented `useLongTextSetting` and the `navigation` array-schema type), settings-first granularity doctrine, heading-line-split and `-text`/`-link` pairing patterns, naming conventions, rename-resets-value gotcha, empty-media gating, key-by-index rule | [references/editable-settings.md](references/editable-settings.md) |
|
|
@@ -423,7 +425,11 @@ var askAi = useNavigationSetting({
|
|
|
423
425
|
- `OPEN_CHAT` — opens Softr's AI chat. **No `destination` or `openIn` needed** — it's the cheapest "Ask AI" button to wire up. **GOTCHA:** Softr's AI pulls context from the block that triggered the chat, NOT from the page. If the block has no data source connected, `chat/prepare` returns HTTP 500 ("Failed to prepare AI assistant") even though the chat panel opens. Fix: in Softr Studio, connect the block to whatever data source the AI should read from — even if the block doesn't read or write any records itself, the connection is what gives the AI context. Verified by direct experiment, May 2026: a button-only helper block with no data source caused this exact failure; connecting it to the same table as the main block fixed it without any code change.
|
|
424
426
|
- `OPEN_URL` — opens an external URL. Needs `destination` (the URL) + `openIn` (`"SELF"` | `"TAB"`).
|
|
425
427
|
- `OPEN_PAGE` — navigates to a Softr page in-app. Needs `destination` (page path) + `openIn` (`"SELF"` | `"TAB"` | `"MODAL"`).
|
|
426
|
-
- `TRIGGER_CUSTOM_WORKFLOW` — runs a Softr workflow.
|
|
428
|
+
- `TRIGGER_CUSTOM_WORKFLOW` — runs a Softr workflow.
|
|
429
|
+
- **Setting shape:** the documented shape is `{ action: "TRIGGER_CUSTOM_WORKFLOW" }` with no `destination`, the same shape as `OPEN_CHAT`. The type also carries an optional `actionId`. The builder picks which workflow it points at. Whether a `destination` is also accepted is not verified live, so don't rely on one.
|
|
430
|
+
- **Calling it from code:** besides `<NavigationAction>`, the live developer guide documents `navigate(setting, { recordId, datasourceId })` ("Runs the workflow with recordId and datasourceId"). It is callable from a mutation's `onSuccess`, e.g. to run a workflow on a record `useRecordCreate` just made (documented 2026-10-05; not yet run end to end by us).
|
|
431
|
+
- **The receiving end** is the Softr Apps trigger **"Run custom workflow"** (`SOFTR_APPS_TRIGGER_WORKFLOW`) in the workflow node catalog. The vibe path documents only `recordId` and `datasourceId` as payload, and the trigger is browser-callable, so have the workflow re-read the record rather than trust its input.
|
|
432
|
+
- Workflows themselves are buildable via MCP; see [references/softr-mcp.md](references/softr-mcp.md#workflows).
|
|
427
433
|
|
|
428
434
|
When the action navigates to a record-specific page, pass the runtime record id via the `recordId` prop on `<NavigationAction>` (not on the setting) so Softr can resolve dynamic URLs:
|
|
429
435
|
|
|
@@ -595,10 +601,13 @@ Non-negotiable rules. Most are enforced by the Softr platform (compiler, validat
|
|
|
595
601
|
endpoint is per block + connection and returns the UNION of every field named by any READ
|
|
596
602
|
`q.select` on that connection, to every viewer. A second or ternary select "only for admins"
|
|
597
603
|
hides nothing. Put a private field on a **second connection of the same table** (allowed) read
|
|
598
|
-
only by a hook non-privileged browsers never run, or in a group-gated block.
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
604
|
+
only by a hook non-privileged browsers never run, or in a group-gated block. Two all-or-nothing
|
|
605
|
+
gates are enforced on these endpoints (list and by-id): page VIEW permission and the **block's own
|
|
606
|
+
Visibility** -- a viewer outside the block's user group gets 403 (verified 2026-10-05). So a block
|
|
607
|
+
gated to a staff group may read unfiltered connections for a staff view. The gate is per block:
|
|
608
|
+
the same table connected to an ungated block, on a page any logged-in user may view, is readable
|
|
609
|
+
by any logged-in user who crafts the request. Source conditions remain the only server-side ROW
|
|
610
|
+
gate. Verified live 2026-09-18 and 2026-10-05. See
|
|
602
611
|
[datasources/multi-datasource.md](datasources/multi-datasource.md#one-connection--one-read-payload-the-union-of-its-selects).
|
|
603
612
|
24. **Multi-datasource: `select:` is a plain module-scope identifier** -- a ternary
|
|
604
613
|
(`select: a ? X : Y`) cannot be attributed to a connection and the query returns `fields: {}`, no
|
package/datasources/hubspot.md
CHANGED
|
@@ -1,51 +1,404 @@
|
|
|
1
1
|
# HubSpot
|
|
2
2
|
|
|
3
|
+
*Rewritten 2026-10-05 from a read-only verification run: five investigations and four adversarial
|
|
4
|
+
fact-checks against a HubSpot-connected demo app on an EU portal, plus Softr's and HubSpot's live
|
|
5
|
+
docs. Labels used below: **verified live** = observed that day through the Softr or HubSpot MCP;
|
|
6
|
+
**documented** = Softr or HubSpot docs say so; **inferred** = reasoned, not observed;
|
|
7
|
+
**unverified** = nobody has tested it. That run made no writes, so nothing on the write side of
|
|
8
|
+
this page is "verified live". It is documented at best.*
|
|
9
|
+
|
|
3
10
|
## Overview
|
|
4
|
-
|
|
11
|
+
|
|
12
|
+
CRM used as a Softr data source, connected by OAuth.
|
|
13
|
+
|
|
14
|
+
- **Softr plan:** Business or Enterprise (documented).
|
|
15
|
+
- **Connecting needs a HubSpot Super Admin** (documented).
|
|
16
|
+
- **HubSpot tier, for what the integration itself reads and writes:** contacts, companies, deals,
|
|
17
|
+
tasks and tickets work on HubSpot Free (documented, HubSpot's product catalog). Custom objects need
|
|
18
|
+
HubSpot Enterprise (documented). HubSpot's *own* workflows need Professional or higher, and
|
|
19
|
+
ticket-based workflows need Service Hub Professional or Enterprise (documented). A Softr workflow
|
|
20
|
+
does not need either ([Workflows](#workflows-softr-workflows-with-hubspot)).
|
|
5
21
|
|
|
6
22
|
## Connection Setup
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
**
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
|
32
|
-
|
|
33
|
-
|
|
|
34
|
-
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
23
|
+
|
|
24
|
+
1. In Softr Studio, go to Data Sources and select HubSpot.
|
|
25
|
+
2. Authenticate via OAuth as a HubSpot Super Admin.
|
|
26
|
+
3. Pick the HubSpot object to connect (see [Supported objects](#supported-objects)).
|
|
27
|
+
4. Some HubSpot fields and objects need **Sensitive Data scopes** for API access (documented). If a
|
|
28
|
+
field you expect comes back empty or missing, check those scopes first.
|
|
29
|
+
|
|
30
|
+
An object appears only if HubSpot grants Softr access to it. Reconnecting restores one that is
|
|
31
|
+
missing (documented).
|
|
32
|
+
|
|
33
|
+
**Through the MCP** (verified live 2026-10-05): `integration_list_databases` on a HubSpot integration
|
|
34
|
+
returns the object ids. For HubSpot the `databaseId`, the `tableId` and the `tableName` are all that
|
|
35
|
+
same object id (`tickets`, `line_items`, …), so pass the object id three times to
|
|
36
|
+
`integration_list_table_fields` and `vibe_coding_block_connect_data_source`.
|
|
37
|
+
|
|
38
|
+
## Supported objects
|
|
39
|
+
|
|
40
|
+
Softr's docs list **15 objects** since softr-public-documentation PR #127 (merged 2026-09-24; the
|
|
41
|
+
list had 7 before). The live integration returned **14 object ids**, i.e. all except Custom Objects,
|
|
42
|
+
which that portal did not have (verified live 2026-10-05).
|
|
43
|
+
|
|
44
|
+
**Listed does not mean usable, and usable does not mean writable.** Softr publishes no per-object
|
|
45
|
+
write matrix, and no vibe-block write to any HubSpot object has been tested.
|
|
46
|
+
|
|
47
|
+
| Object | Id | Primary field | What to know |
|
|
48
|
+
|---|---|---|---|
|
|
49
|
+
| Contacts | `contacts` | `hs_full_name_or_email` | HubSpot Free. The natural users table. Has no `associatedcompanyid` in Softr (see [Field model](#field-model)) |
|
|
50
|
+
| Companies | `companies` | `name` | HubSpot Free |
|
|
51
|
+
| Deals | `deals` | `dealname` | HubSpot Free. Stage ids are portal-specific |
|
|
52
|
+
| Tickets | `tickets` | `subject` | HubSpot Free. A create needs `subject` and `hs_pipeline_stage` ([Writing](#writing)) |
|
|
53
|
+
| Tasks | `tasks` | `hs_task_subject` | HubSpot Free |
|
|
54
|
+
| Notes | `notes` | `hs_body_preview` | A create needs `hs_timestamp`. HubSpot says a note should be associated with at least one record, so notes depend on the [association-write question](#association-writes--an-open-question) |
|
|
55
|
+
| Leads, Listings, Appointments | `leads`, `listings`, `appointments` | — | Listed live; not examined in the 2026-10-05 run |
|
|
56
|
+
| Projects | `projects` | `hs_name` | **Must be activated in HubSpot by a Super Admin** (Data Management > Data Model; documented). On a portal where it apparently wasn't, Softr still listed the object, but its `hs_pipeline` / `hs_pipeline_stage` had **zero choices** (verified live). HubSpot requires both on create (documented), so no create can succeed there |
|
|
57
|
+
| Products | `products` | `name` | Catalog entries. Links go only to deals and to other products, none to companies or contacts |
|
|
58
|
+
| Line Items | `line_items` | `name` | **Need a parent object** (deal, quote, subscription, invoice or payment link; documented). No links to companies or contacts |
|
|
59
|
+
| Invoices | `invoices` | `hs_number` | **Need Commerce Hub** (documented). A draft needs only `hs_currency`; moving one to Open needs an associated contact and at least one line item (HubSpot API docs) |
|
|
60
|
+
| Subscriptions | `subscriptions` | `hs_name` | **Need Commerce Hub. Treat as read-only:** HubSpot's API needs HubSpot payments or Stripe for writes, and it cannot set associations at all (documented) |
|
|
61
|
+
| Custom Objects | — | — | **Need HubSpot Enterprise** (documented). Appear only when they exist in the portal |
|
|
62
|
+
|
|
63
|
+
Softr's Workflows HubSpot page says Add / Update / Delete work "across every HubSpot object". That is
|
|
64
|
+
a marketing line. HubSpot's own API rules above contradict it for subscriptions, and for projects
|
|
65
|
+
until activation. Don't quote it to a client as a write guarantee.
|
|
66
|
+
|
|
67
|
+
**Native blocks vs vibe blocks.** Softr's HubSpot page says "You can connect only one object type to
|
|
68
|
+
one block". That rule is for native blocks. A vibe block connects several objects with
|
|
69
|
+
`datasource.define` plus `from:` on every hook (see
|
|
70
|
+
[multi-datasource.md](multi-datasource.md)).
|
|
71
|
+
|
|
72
|
+
## Field model
|
|
73
|
+
|
|
74
|
+
*Verified live 2026-10-05 via `integration_list_table_fields` and `vibe_coding_block_get_settings`,
|
|
75
|
+
unless marked otherwise.*
|
|
76
|
+
|
|
77
|
+
- **`fieldReferenceKey` is `"id"`.** `q.select()` uses HubSpot internal property names (`firstname`,
|
|
78
|
+
`dealstage`, `hs_pipeline_stage`). Custom properties use the internal name HubSpot gave them.
|
|
79
|
+
- **Associations are `LINKED_RECORD` fields named `associations.<x>`**, with
|
|
80
|
+
`options.linkedTableId` set to the target object. Examples:
|
|
81
|
+
- deals: `associations.company` ("Associated Company"), `associations.contact`,
|
|
82
|
+
`associations.deal_to_company` ("Associated Primary Company")
|
|
83
|
+
- tickets: `associations.company`, `associations.contact`, `associations.ticket_to_company`
|
|
84
|
+
("Associated Primary Company"), `associations.deal`. Tickets have 13 in all.
|
|
85
|
+
- companies: `associations.company_to_deal`, `associations.company_to_ticket`,
|
|
86
|
+
`associations.company_to_contact` (the "with Primary Company" links), 20 in all.
|
|
87
|
+
|
|
88
|
+
Read them as you would any linked field: an association can arrive as a single `{ id, label }`
|
|
89
|
+
object as well as an array of them, so normalise before you `.map()` (see
|
|
90
|
+
[reading.md](reading.md#filtering-by-a-linked-record-server-side)). The single-object case was
|
|
91
|
+
seen in a Softr workflow's Find record output; the vibe-hook read shape for HubSpot is unverified.
|
|
92
|
+
- **SELECT fields carry id→label choices** in `options.choices`, and HubSpot ids are not labels.
|
|
93
|
+
- **Deals:** stage ids are portal-specific. The live portal had `6183367908` = Initial Contact
|
|
94
|
+
… `closedwon` = Closed Won, in pipeline `default` ("Sales Pipeline").
|
|
95
|
+
- **Tickets:** they use `hs_pipeline_stage`, which held `1` New, `2` Waiting on contact,
|
|
96
|
+
`3` Waiting on us and `4` Closed, all in pipeline `0` ("Support Pipeline"). That is a fresh
|
|
97
|
+
portal's default, so read the choices from the schema rather than hardcoding them.
|
|
98
|
+
- **Owners:** `hubspot_owner_id` choices are numeric HubSpot owner ids, labelled
|
|
99
|
+
"Name (email)".
|
|
100
|
+
- **`hubspot_owner_email` and `hubspot_owner_name` are fields Softr derives, not HubSpot
|
|
101
|
+
properties.** HubSpot returns them as `propertiesNotFound`, yet they appear as SELECT fields on
|
|
102
|
+
every object. Filtering on them is untested, because HubSpot's search has no such property to
|
|
103
|
+
filter on. Filter and write ownership through **`hubspot_owner_id`**, which is the real property.
|
|
104
|
+
- **Softr exposes only a subset of HubSpot's properties.** Tickets showed 36 of about 122 HubSpot
|
|
105
|
+
properties, plus the 2 derived owner fields; invoices showed 26 of about 94. Missing on tickets
|
|
106
|
+
are `hs_all_associated_contact_emails`, `hs_v2_date_entered_*`, `hs_resolution`, `hs_ticket_id`,
|
|
107
|
+
`hs_tag_ids` and `hs_object_source_label`. Softr's docs say "All default HubSpot object
|
|
108
|
+
properties" are supported; the live schema contradicts that. **Custom properties are exposed:**
|
|
109
|
+
the portal's custom ticket properties (`issue_category`, `issue_severity`) were in the schema.
|
|
110
|
+
Before designing around a property, confirm it is in the field list.
|
|
111
|
+
- **Field metadata has no read-only flag.** Each field carries only `id`, `name`, `type` and
|
|
112
|
+
`options`. HubSpot-computed properties (`hs_object_id`, `createdate`, `hs_lastmodifieddate`,
|
|
113
|
+
`num_associated_*`) look exactly like writable ones.
|
|
114
|
+
- **Contacts have no `associatedcompanyid` in Softr**, and HubSpot also returned it as not found. A
|
|
115
|
+
contact reaches its company only through `associations.company` /
|
|
116
|
+
`associations.contact_to_company`, or through the free-text `company` property.
|
|
117
|
+
- `hs_file_upload` on tickets is an ATTACHMENT in Softr but a plain string property in HubSpot.
|
|
118
|
+
Writing it is untested.
|
|
119
|
+
|
|
120
|
+
## Writing
|
|
121
|
+
|
|
122
|
+
| Field kind | Writable? | Evidence |
|
|
123
|
+
|---|---|---|
|
|
124
|
+
| Default properties | Yes | Documented (Softr's HubSpot page); untested from a vibe block |
|
|
125
|
+
| Custom properties | Yes | Documented; untested from a vibe block |
|
|
126
|
+
| Softr computed fields (Calculation, Rollup, Count, Formula) | Read-only | Documented. These are the **only** fields Softr's page lists as read-only |
|
|
127
|
+
| HubSpot-computed properties (`hs_object_id`, `createdate`, `hs_lastmodifieddate`, `num_associated_*`) | Presumably not | Inferred from HubSpot; nothing in Softr's metadata says so |
|
|
128
|
+
| Associations (`associations.*`) | **Unverified** | [Open question below](#association-writes--an-open-question) |
|
|
129
|
+
| `hubspot_owner_email` / `hubspot_owner_name` | Don't | Softr-derived; write `hubspot_owner_id` instead (inferred) |
|
|
130
|
+
|
|
131
|
+
- **Ticket create:** HubSpot requires `subject` and `hs_pipeline_stage`. `hs_pipeline` is optional
|
|
132
|
+
when the portal has one ticket pipeline, because the default is used (documented, HubSpot API).
|
|
133
|
+
**Note create:** `hs_timestamp` is required (documented).
|
|
134
|
+
- **SELECT write format is unknown for HubSpot.** On Softr Database, vibe hooks write a SELECT by
|
|
135
|
+
its **label** (verified 2026-08-25; see [writing.md](writing.md#dropdown--single-select-softr-database)).
|
|
136
|
+
On HubSpot the id and the label differ (`'1'` vs `'New'`, `'6183367908'` vs `'Initial Contact'`),
|
|
137
|
+
and which one the connector accepts has not been tested. Test both on a throwaway record before
|
|
138
|
+
building a form on it.
|
|
139
|
+
|
|
140
|
+
### Association writes — an open question
|
|
141
|
+
|
|
142
|
+
**Unverified.** This page used to say "Associations are read-only in Vibe Coding blocks". That
|
|
143
|
+
statement had no source and was removed on 2026-10-05. What is actually known:
|
|
144
|
+
|
|
145
|
+
- Softr's HubSpot page lists only computed fields as read-only. It documents associations for
|
|
146
|
+
display only: the Linked List block, the Associated Object IDs property, and visibility
|
|
147
|
+
conditional filters. It never says they are read-only (documented; the January 2025 version reads
|
|
148
|
+
the same).
|
|
149
|
+
- Softr's generic "Mapping Fields for Actions" page auto-maps "every writable column" into native
|
|
150
|
+
Add / Update record forms. Linked-record fields "are included", as dropdowns or multi-selects, and
|
|
151
|
+
computed fields are skipped (documented, not HubSpot-specific).
|
|
152
|
+
- A Softr staff post in the community HubSpot beta thread (2024-03-26) announced "You can now
|
|
153
|
+
associate Tickets with other Tickets". That is a community announcement about native blocks, not
|
|
154
|
+
documentation.
|
|
155
|
+
- HubSpot's ticket-create API accepts inline associations, so the connector *could* write them
|
|
156
|
+
(documented, HubSpot side).
|
|
157
|
+
- No vibe-block test exists, and the field metadata cannot settle it.
|
|
158
|
+
|
|
159
|
+
**If you are cleared to run a write test** (it creates real HubSpot records, so only on a disposable
|
|
160
|
+
portal or with the owner's go-ahead):
|
|
161
|
+
|
|
162
|
+
1. Create one **plain** ticket first (`subject`, `hs_pipeline_stage`). This settles the
|
|
163
|
+
SELECT id-vs-label question on its own.
|
|
164
|
+
2. Then add `associations.company` / `associations.contact` as `["<id>"]` (the Softr Database
|
|
165
|
+
string-array shape), falling back to `[{ id }]` (the Airtable shape); see
|
|
166
|
+
[writing.md](writing.md#linked-record-format-for-mutations). Check with
|
|
167
|
+
`vibe_coding_block_get_settings` that the ADD_RECORD action still exists and lists the
|
|
168
|
+
association field. On Airtable, an unresolvable field in a create select silently disables the
|
|
169
|
+
whole action ([airtable.md](airtable.md#maintainability-gotcha)); whether a non-writable
|
|
170
|
+
association does the same is unknown.
|
|
171
|
+
3. Check the result in HubSpot: was the link created, and is `associations.company` the unlabeled
|
|
172
|
+
link (type 339) or the primary one (26)? Repeat with `useRecordUpdate`, to learn whether it adds
|
|
173
|
+
links or replaces them.
|
|
174
|
+
|
|
175
|
+
**Documented fallback: a Softr workflow calls HubSpot's associations API.**
|
|
176
|
+
|
|
177
|
+
- **The step:** a **Run custom code** step (`CUSTOM_CODE` v1.2.0) with the HubSpot integration
|
|
178
|
+
attached. `fetch()` calls to `api.hubapi.com` then carry that integration's credentials, with no
|
|
179
|
+
token in the code.
|
|
180
|
+
- **Plan and testing:** the step needs a paid Softr plan, and it is `REAL_ONLY`, so a test run
|
|
181
|
+
writes for real.
|
|
182
|
+
- **Limits:** about 2 minutes per run, and up to 20 fetches per second.
|
|
183
|
+
- **The HubSpot calls** (associations v4):
|
|
184
|
+
- Unlabeled: `PUT /crm/objects/2026-09/ticket/{ticketId}/associations/default/company/{companyId}`,
|
|
185
|
+
and the same pattern for `contact`.
|
|
186
|
+
- Labeled, e.g. primary company: `PUT /crm/objects/2026-09/ticket/{ticketId}/associations/company/{companyId}`
|
|
187
|
+
with body `[{ "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 26 }]`.
|
|
188
|
+
- Type ids: ticket→contact **16**, ticket→company **339**, ticket→primary company **26**.
|
|
189
|
+
- `DELETE` on the same path unlinks.
|
|
190
|
+
- HubSpot's ticket-create endpoint also accepts an `associations` array, so one step can create
|
|
191
|
+
the ticket already linked.
|
|
192
|
+
- **Starting it from the block:** call `navigate(setting, { recordId, datasourceId })` on a
|
|
193
|
+
`TRIGGER_CUSTOM_WORKFLOW` setting inside `useRecordCreate`'s `onSuccess` (see SKILL.md's
|
|
194
|
+
NavigationAction section). The vibe path documents only `recordId` and `datasourceId` as payload.
|
|
195
|
+
The trigger is a browser-callable endpoint, so the workflow should re-read the ticket from HubSpot
|
|
196
|
+
rather than trust what it receives.
|
|
197
|
+
- **No block wiring:** a `HUBSPOT_RECORD_CREATED` trigger on tickets plus the same custom-code step
|
|
198
|
+
can link each new ticket by a requester-email property. This also catches tickets created in
|
|
199
|
+
HubSpot itself. The cost is trigger latency (unknown, see below) and a run for every new ticket.
|
|
200
|
+
- **Why the block can't do it itself:** `useProxyFetch` is documented for REST API sources only.
|
|
201
|
+
Call API authenticates only REST_API integrations and needs Professional or higher, so it would
|
|
202
|
+
need a HubSpot private-app token stored as a REST integration (inferred).
|
|
203
|
+
- **HubSpot-side route:** a HubSpot workflow's "Create associations" action needs Pro or
|
|
204
|
+
Enterprise, and ticket-based workflows need Service Hub Pro or Enterprise (documented). It
|
|
205
|
+
matches records by exact, case-sensitive property value.
|
|
206
|
+
|
|
207
|
+
## Row scoping — who sees which records
|
|
208
|
+
|
|
209
|
+
A connection's **Source conditions are the only server-side row gate**. A `where` in block code is a
|
|
210
|
+
request parameter the caller controls, not access control. See
|
|
211
|
+
[../references/softr-mcp.md](../references/softr-mcp.md#what-the-server-enforces-on-a-blocks-data-endpoints)
|
|
212
|
+
(verified 2026-09-18 on Softr Database; on HubSpot 2026-10-05, below).
|
|
213
|
+
|
|
214
|
+
The **block's Visibility** is enforced on the same endpoints, all or nothing (verified 2026-10-05 on
|
|
215
|
+
HubSpot). A block gated to an "Account managers" condition group returned every deal and ticket to
|
|
216
|
+
members from connections with no Source condition, and 403 on list and by-id to clients, a
|
|
217
|
+
Softr-only user and logged-out visitors. So a staff view can be a group-gated block with
|
|
218
|
+
unfiltered connections. The same tables on an ungated block are open to anyone who may view the
|
|
219
|
+
page ([details](multi-datasource.md#one-connection--one-read-payload-the-union-of-its-selects)).
|
|
220
|
+
|
|
221
|
+
- **Scope clients by company with the user's own field** (verified 2026-10-05). A Source
|
|
222
|
+
condition `associations.company IS_ONE_OF ["USER:::associations.company"]`, logical operator
|
|
223
|
+
AND, on the deals and the tickets connections gave each client only their own companies'
|
|
224
|
+
records: Dana 1 deal and 1 ticket, Tom 1 deal. The users table is the contacts table, so
|
|
225
|
+
`USER:::associations.company` is the logged-in contact's own company association. The token is
|
|
226
|
+
`USER:::<user field id>`, **no braces**, as the entire value. It was set in Studio's Source tab
|
|
227
|
+
and over MCP alike ([details](../references/softr-mcp.md#logged-in-user-values-in-source-conditions)).
|
|
228
|
+
- **It fails closed.** A synced contact with no company, a Softr-only user and account managers
|
|
229
|
+
without a company all got 0 rows. By-id reads of other companies' records returned 404.
|
|
230
|
+
- **So staff need their own block**, gated to their group, with unfiltered connections (above).
|
|
231
|
+
Widening the client condition for them would widen it for everyone.
|
|
232
|
+
- **Use AND.** With one rule OR and AND behave the same, but a second rule added under OR widens
|
|
233
|
+
access.
|
|
234
|
+
- **The email token is different: `{USER:::EMAIL}`, with braces**, as the entire value. It is
|
|
235
|
+
verified on Softr Database and untested on HubSpot. For any other user field, pick it once in the
|
|
236
|
+
block's Source tab and read `dataSources[].condition` back with `vibe_coding_block_get_settings`.
|
|
237
|
+
- **Community evidence for association scoping in native blocks.** In
|
|
238
|
+
[community.softr.io/t/hubspot-conditional-filter/10572](https://community.softr.io/t/hubspot-conditional-filter/10572)
|
|
239
|
+
(September 2024), a native filter "ticket's Associated Company ID = logged-in user's Associated
|
|
240
|
+
Company ID" worked after a Softr fix. It used an older field model; `associatedcompanyid` is no
|
|
241
|
+
longer on Softr's contacts. Softr's HubSpot page also says "You can also use associated objects
|
|
242
|
+
in Visibility Conditional Filters". The vibe-block version above is the verified one.
|
|
243
|
+
- **Fallback: put the user's email on the records**, for scoping that no user field can express. Add
|
|
244
|
+
a custom text property, e.g. "Portal requester email" on tickets or "Account manager email" on
|
|
245
|
+
companies and deals. Write it when the record is created, and compare it with `{USER:::EMAIL}`
|
|
246
|
+
using **IS**. If the property holds a list of emails and you use CONTAINS, mind the substring
|
|
247
|
+
trap: `bob@x.com` also matches `jbob@x.com` (verified 2026-09-18 on Softr Database). The value is
|
|
248
|
+
written by the browser, so a determined user could tamper with it on create. That is acceptable
|
|
249
|
+
for scoping a demo; production needs a server-side source of identity. The user's company
|
|
250
|
+
association above lives in HubSpot instead, out of reach of a block with no contact write action.
|
|
251
|
+
- **Owner scoping.** `hubspot_owner_id` is the real property, but on a contact it names that
|
|
252
|
+
contact's owner, not the contact. Scoping "my deals" for an account manager would need a custom
|
|
253
|
+
contact property holding their own owner id, read through `USER:::<field id>` (untested). For a
|
|
254
|
+
team-wide staff view, a group-gated block with unfiltered connections needs no owner filter at
|
|
255
|
+
all. Owners are also HubSpot users, i.e. HubSpot seats.
|
|
256
|
+
|
|
257
|
+
**Filter limits** ([docs.softr.io/troubleshooting/troubleshooting-hubspot-errors](https://docs.softr.io/troubleshooting/troubleshooting-hubspot-errors)):
|
|
258
|
+
|
|
259
|
+
- **Softr's numbers:** 5 groups × 6 filters, 18 filters per block in total, and only **4 AND
|
|
260
|
+
filters on a block with an Edit button**.
|
|
261
|
+
- **What counts:** conditional filters, inline search and filter, and action-visibility filters
|
|
262
|
+
(documented). A code `where` presumably lands in the same HubSpot search request (inferred).
|
|
263
|
+
- **AND and OR are swapped on Softr's page.** It calls the groups "AND" and the filters inside them
|
|
264
|
+
"OR". HubSpot's search API is the other way round: filters inside a group are ANDed and groups are
|
|
265
|
+
ORed, with the same 5 / 6 / 18 caps (documented).
|
|
266
|
+
- **HubSpot search also caps:** a query returns at most 10,000 results (documented). Associations
|
|
267
|
+
are filtered through the `associations.{objectType}` pseudo-property, which does not cover
|
|
268
|
+
custom-object associations (documented).
|
|
269
|
+
- **The MCP's record-filter tool** takes one flat AND/OR list per connection, and each value is an
|
|
270
|
+
array of strings. ARRAY fields allow `IS`, `IS_ONE_OF`, `IS_NONE_OF`, `HAS_ALL_OF`, `IS_EMPTY` and
|
|
271
|
+
`IS_NOT_EMPTY`, with no `CONTAINS` (verified 2026-10-05 from the tool's description).
|
|
272
|
+
|
|
273
|
+
## Rate limits
|
|
274
|
+
|
|
275
|
+
- **110 requests per 10 seconds per HubSpot account** for apps distributed through the HubSpot
|
|
276
|
+
Marketplace. The search API is counted separately, and HubSpot's API-limit add-on does not raise
|
|
277
|
+
this (documented). That Softr's connection counts as such an app is **inferred**: Softr has a
|
|
278
|
+
Marketplace listing.
|
|
279
|
+
- **The CRM search API allows 5 requests per second per account** (documented). How Softr turns
|
|
280
|
+
hook calls into HubSpot calls is not documented. If list reads go through search, several
|
|
281
|
+
HubSpot connections on one page, times concurrent users, can reach that limit (inferred).
|
|
282
|
+
- **New and updated records take "a few moments" to appear in search results** (documented). A
|
|
283
|
+
`refetch()` right after a mutation may therefore still return the old value (inferred). Prefer
|
|
284
|
+
updating the UI optimistically from the mutation's own result.
|
|
285
|
+
- Softr also caches data-source reads ("short-term" on one docs page, "24 hour" on another; scope
|
|
286
|
+
unspecified). See [overview.md](overview.md).
|
|
287
|
+
|
|
288
|
+
## User sync
|
|
289
|
+
|
|
290
|
+
- **HubSpot supports 2-way user sync** (documented). Email is required and is the unique
|
|
291
|
+
identifier. Name, Magic link, Avatar, Created date and Last seen date map too. All other contact
|
|
292
|
+
properties remain usable in user groups, conditional filters and `useCurrentUser({ properties })`.
|
|
293
|
+
- **Filtered sync** ("Selected", with rules) is available (documented; listed under Business on
|
|
294
|
+
Softr's pricing page).
|
|
295
|
+
- **Deleting a Softr user deletes the HubSpot contact.** Deleting in HubSpot does not delete the
|
|
296
|
+
Softr user (documented). The filtered-sync option "Delete their user account" also removes the
|
|
297
|
+
data-source record (documented).
|
|
298
|
+
- **`createUserInDatasource` defaults to `true`** on `application_create_user_connection`, so users
|
|
299
|
+
added in Softr create HubSpot contacts. To keep people out of HubSpot (e.g. internal account
|
|
300
|
+
managers), use the "Let the user only exist in the Softr app" option (documented).
|
|
301
|
+
- **Sync is continuous only on a published app.** In Studio, or on an unpublished app, it runs on
|
|
302
|
+
login, on publish and when the Users tab is opened (documented).
|
|
303
|
+
- **Even on a published app the lag varies** (verified 2026-10-05). A new HubSpot contact appeared
|
|
304
|
+
as a Softr user after 33 s in the morning; two more, created later that day, took 24 min. Never
|
|
305
|
+
assume real time: confirm the user exists with `application_list_users` before testing as them.
|
|
306
|
+
- **An app has one users table** (documented). Account managers are therefore either contacts in the
|
|
307
|
+
same table or Softr-only users.
|
|
308
|
+
- **`useCurrentUser().id` exists only when user sync is on** (documented). Whether it equals the
|
|
309
|
+
HubSpot contact id is **unverified**.
|
|
310
|
+
- **User Caching** (App Settings > Advanced) keeps the **logged-in user's own record** for 2 minutes.
|
|
311
|
+
Softr recommends leaving it on, and suggests turning it off when user-group membership updates
|
|
312
|
+
slowly (documented). It affects group membership and logged-in-user values, not how fresh other
|
|
313
|
+
records are. A ticket-status lag comes from elsewhere: the search delay above, data-source
|
|
314
|
+
caching, or workflow trigger latency.
|
|
315
|
+
- **Condition-based user groups on a synced HubSpot property work** (verified 2026-10-05). For a
|
|
316
|
+
select contact property the rule is subject `USER:portal_role` (the property's internal name),
|
|
317
|
+
type `ARRAY`, operator `IS_ONE_OF`, value `[<choice id>]`: the choice id, not the label. That
|
|
318
|
+
syntax is for **user groups**. In a block's Source condition the subject form returned 400; there
|
|
319
|
+
the user field goes in the value, as `USER:::<field id>` (see above). Membership then lives in
|
|
320
|
+
HubSpot: anyone who can edit that property there can grant the group's access.
|
|
321
|
+
- **`application_list_users` is not a membership check.** It showed `userGroups: []` for every
|
|
322
|
+
user, including members of condition groups that demonstrably applied (verified 2026-10-05).
|
|
323
|
+
Test membership by what the user can reach, e.g. preview as them against a group-gated block.
|
|
324
|
+
|
|
325
|
+
## Audit trail
|
|
326
|
+
|
|
327
|
+
- **HubSpot records a connected app's writes with change source "Integration"**, not the person who
|
|
328
|
+
made the edit. Only edits made in HubSpot's own UI show a user's name and email (documented). A
|
|
329
|
+
2022 HubSpot developer changelog adds that the app's ID is recorded.
|
|
330
|
+
- **Integration writes cannot be restored with one click.** Property history offers Restore only for
|
|
331
|
+
CRM UI edits, imports and workflows (documented).
|
|
332
|
+
- What HubSpot puts in "Updated by user ID" for a Softr write is **unknown**. Records written by a
|
|
333
|
+
different connector carried an ID that belonged to no user in the portal (verified live; not a
|
|
334
|
+
Softr write).
|
|
335
|
+
- **Workaround:** write a "Last edited in Softr by" custom property from `useCurrentUser().email`
|
|
336
|
+
on every create and update. The value is self-reported, because the browser sends it. That is fine
|
|
337
|
+
for a trail, but it is not security.
|
|
338
|
+
- Emails sent by a Softr workflow's Send email step don't appear on the HubSpot record's timeline.
|
|
339
|
+
Logging them as a note would need an association write (inferred).
|
|
340
|
+
|
|
341
|
+
## Workflows (Softr Workflows with HubSpot)
|
|
342
|
+
|
|
343
|
+
*Catalog and specs verified live 2026-10-05.*
|
|
344
|
+
|
|
345
|
+
- **Triggers: `HUBSPOT_RECORD_CREATED` and `HUBSPOT_RECORD_UPDATED`** (v1.0.0, both `REAL_ONLY`).
|
|
346
|
+
- Their only inputs are `integrationId`, `type` (the object id, e.g. `"tickets"`) and an optional
|
|
347
|
+
`objectTypeId`.
|
|
348
|
+
- There is **no property filter**. Softr Tables and Airtable "record updated" triggers have an
|
|
349
|
+
`updateField` option; these don't.
|
|
350
|
+
- Whether a data-source integration connected before triggers existed can run them is untested.
|
|
351
|
+
Zoho's docs say to reconnect in that case.
|
|
352
|
+
- **Latency is unknown.**
|
|
353
|
+
- Neither trigger appears on Softr's Trigger Types page, and the HubSpot Workflows page lists
|
|
354
|
+
actions only.
|
|
355
|
+
- The Trigger Types page says only Softr Tables, Calendly, Attio and Zoho CRM are instant and
|
|
356
|
+
"other services use background polling". That sentence is wrong for Stripe (Softr registers a
|
|
357
|
+
webhook) and for DocuSign (pushed by Docusign Connect), so it can't be relied on for HubSpot
|
|
358
|
+
either.
|
|
359
|
+
- Time a real run before promising anything.
|
|
360
|
+
- **Record updated fires on any change to any record of that object.** Expect it to fire on
|
|
361
|
+
HubSpot's internal recalculations and on Softr's own writes too (inferred). Build two things in
|
|
362
|
+
from the start:
|
|
363
|
+
- **A stage filter.** Note that a filter on the *current* stage alone fires again on every later
|
|
364
|
+
edit to a ticket already in that stage.
|
|
365
|
+
- **A dedupe property**, e.g. `softr_last_notified_stage`: filter on `stage != it`, and set it
|
|
366
|
+
after acting. Softr exposes no `hs_v2_date_entered_*` property to tell a stage entry apart from
|
|
367
|
+
other edits.
|
|
368
|
+
- **Actions:** Add record (`MOCK_AND_REAL`), Update record (`REAL_ONLY`), Update multiple records,
|
|
369
|
+
Delete record, Delete multiple records, Find record and Find multiple records.
|
|
370
|
+
- **There is no associate action.**
|
|
371
|
+
- Add and Update take a `fields` input of type `DATA_SOURCE_FIELDS`. Whether `associations.*`
|
|
372
|
+
is offered there, or actually written, is unknown.
|
|
373
|
+
- **Find record output** is `{ id, fields }`. Associations arrive as lists of `{ id, label }`
|
|
374
|
+
(e.g. `fields["associations.contact"][*].id`), though some single-label associations come back
|
|
375
|
+
as one `{ id, label }` object. SELECT fields arrive as `{ id, label }`, so compare on `.id`. This
|
|
376
|
+
was seen on a companies sample from a mock test; the same shape on tickets is inferred.
|
|
377
|
+
- Workflow nodes call the workspace's HubSpot integration directly, with `type` set to the object
|
|
378
|
+
id, so an object need not be connected to any block for a workflow to use it.
|
|
38
379
|
|
|
39
380
|
## Gotchas
|
|
40
|
-
|
|
41
|
-
- **
|
|
42
|
-
|
|
43
|
-
- **
|
|
44
|
-
|
|
381
|
+
|
|
382
|
+
- **Listed is not usable.** Projects needs activation, Subscriptions are read-only, Invoices and
|
|
383
|
+
Subscriptions need Commerce Hub, Line Items need a parent, Custom Objects need HubSpot Enterprise.
|
|
384
|
+
- **Association writes are unverified**, not "read-only". Don't build a form that depends on them
|
|
385
|
+
until a write test has passed. The fallback is a workflow with Run custom code.
|
|
386
|
+
- **SELECT ids ≠ labels**, and the write format is unknown. Take display labels from the schema's
|
|
387
|
+
choices (`useFieldOptions` should serve them, but that is untested on HubSpot); don't hardcode
|
|
388
|
+
stage ids across portals.
|
|
389
|
+
- **The owner email and name fields are Softr's, not HubSpot's.** Filter and write
|
|
390
|
+
`hubspot_owner_id`.
|
|
391
|
+
- **Only part of each object's properties is exposed.** Check a property is in the field list
|
|
392
|
+
before relying on it.
|
|
393
|
+
- **Mind the filter budget on blocks with an Edit button:** 4 AND filters, counting Source
|
|
394
|
+
conditions, inline search and action-visibility filters together.
|
|
395
|
+
- **Scope client rows with `USER:::associations.company`, no braces,** in a Source condition
|
|
396
|
+
joined by AND. It fails closed, so give staff their own group-gated block.
|
|
45
397
|
|
|
46
398
|
## Best For
|
|
399
|
+
|
|
400
|
+
- Client and partner portals on top of HubSpot-managed contacts and companies
|
|
401
|
+
- Support ticket portals
|
|
47
402
|
- Sales portals and deal rooms
|
|
48
403
|
- CRM dashboards for teams or clients
|
|
49
|
-
- Support ticket portals
|
|
50
|
-
- Partner portals with HubSpot-managed contacts
|
|
51
404
|
- Any app where HubSpot is the system of record
|
|
@@ -91,7 +91,7 @@ it behaves as a **union** of both branches, not a choice between them. Which is
|
|
|
91
91
|
|
|
92
92
|
## One connection = one read payload (the union of its selects)
|
|
93
93
|
|
|
94
|
-
*Verified live 2026-09-18.*
|
|
94
|
+
*Verified live 2026-09-18; the block-visibility gate 2026-10-05.*
|
|
95
95
|
|
|
96
96
|
The records endpoint is per block + connection —
|
|
97
97
|
`/blocks/<blockId>/datasources/<dataSourceId>/records` — and it returns the **UNION of every field
|
|
@@ -131,11 +131,20 @@ function AdminNotes({ recordId }) {
|
|
|
131
131
|
Or put the private field in a separate block whose visibility is group-gated.
|
|
132
132
|
|
|
133
133
|
Know what this buys you. Not *rendering* the hook keeps the field out of ordinary browsers, but
|
|
134
|
-
the endpoint still exists
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
134
|
+
the endpoint still exists. Two all-or-nothing gates are enforced on it, list and by-id: page VIEW
|
|
135
|
+
permission, and the **block's own Visibility** (`predefinedUserGroup` + `customUserGroupIds`). A
|
|
136
|
+
viewer outside either gets a 403 (block gate verified 2026-10-05, on HubSpot). Past those gates, on
|
|
137
|
+
an ungated block on a page any logged-in user may view, **every connected datasource is readable by
|
|
138
|
+
any logged-in user who crafts the request**. A connection's **Source conditions are the only
|
|
139
|
+
server-side ROW gate**; the only server-side gate on the *field* is a page or block the viewer
|
|
140
|
+
cannot see.
|
|
141
|
+
|
|
142
|
+
**A block gated to a user group may therefore read unfiltered connections** (verified 2026-10-05).
|
|
143
|
+
A staff dashboard gated to an "Account managers" group, with no Source condition on its deals and
|
|
144
|
+
tickets connections, gave both members all 41 deals and 16 tickets, and gave clients, a Softr-only
|
|
145
|
+
user and logged-out visitors 403 on list and by-id. The gate belongs to the block, not the table:
|
|
146
|
+
connect the same table to an ungated block and it is open again. See
|
|
147
|
+
[../references/softr-mcp.md](../references/softr-mcp.md#what-the-server-enforces-on-a-blocks-data-endpoints).
|
|
139
148
|
|
|
140
149
|
Alias → field attribution is per connection, so two selects on different connections may reuse
|
|
141
150
|
an alias name (`customer` on both) without colliding — including in `where` filters.
|
package/datasources/overview.md
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
- Data is connected to **dynamic blocks** (List, Grid, Table, Kanban, Chart, Form, etc.) in the Softr Studio.
|
|
33
33
|
- **Multiple data sources** can coexist in a single Softr app, even on the same page.
|
|
34
34
|
- Softr IP addresses to whitelist for secured databases: `3.120.79.212`, `3.123.159.186`, `52.58.246.121`
|
|
35
|
-
- **AI-assisted schema discovery via MCP:** the official Softr MCP server (`https://mcp.softr.io/mcp`) reads schema and field references directly from the workspace -- Softr Databases in full, plus browsable field-level access to connected **Airtable, Google Sheets, Notion, and
|
|
35
|
+
- **AI-assisted schema discovery via MCP:** the official Softr MCP server (`https://mcp.softr.io/mcp`) reads schema and field references directly from the workspace -- Softr Databases in full, plus browsable field-level access to connected external integrations, among them **Airtable, Google Sheets, Notion, Supabase and HubSpot** (HubSpot browsed live 2026-10-05; the full type list is in softr-mcp.md). Sources the MCP cannot browse still need the manual workflows in [fields.md](fields.md). See [../references/softr-mcp.md](../references/softr-mcp.md).
|
|
36
36
|
|
|
37
37
|
## User Sync Availability
|
|
38
38
|
|
package/datasources/reading.md
CHANGED
|
@@ -308,6 +308,13 @@ var user = useCurrentUser();
|
|
|
308
308
|
// Note: `id` is only present when user sync is enabled.
|
|
309
309
|
```
|
|
310
310
|
|
|
311
|
+
**`where: q.text("ownerEmail").is(user.email)` is not access control.** It shapes the UI, but the
|
|
312
|
+
caller controls that parameter. To restrict rows to the logged-in user on the server, put the
|
|
313
|
+
condition in the connection's Source conditions, as the whole value: `{USER:::EMAIL}` (with
|
|
314
|
+
braces) for the user's email, or `USER:::<user field id>` (no braces) for one of the user's own
|
|
315
|
+
fields, e.g. their company (verified 2026-10-05 on HubSpot; fails closed when the field is empty).
|
|
316
|
+
See [../references/softr-mcp.md](../references/softr-mcp.md#logged-in-user-values-in-source-conditions).
|
|
317
|
+
|
|
311
318
|
**Custom user-record fields** are first-class: pass a `properties` map (aliased like a `select` query) and read them under `user.properties`:
|
|
312
319
|
|
|
313
320
|
```jsx
|
package/datasources/writing.md
CHANGED
|
@@ -16,6 +16,13 @@ Record mutations, sequential write queues, file uploads, linked record format, a
|
|
|
16
16
|
|
|
17
17
|
Every Vibe Coding block in Softr Studio has an **Actions tab** alongside Chat / Source / Content / Visibility. The Actions tab is a **read-only inspector** of the Create / Update / Delete operations the platform inferred from your code.
|
|
18
18
|
|
|
19
|
+
> **Docs discrepancy (noted 2026-10-05, unresolved).**
|
|
20
|
+
> - **What the docs say:** the live Vibe Coding page tells builders to "Review each action and configure permissions, conditions, or connected workflows as needed".
|
|
21
|
+
> - **Where we agree:** permissions are settable, on the Actions tab or with `vibe_coding_block_set_action_visibility`, so "read-only" is about the action *list*, which only the code changes.
|
|
22
|
+
> - **What nobody has observed:** an Actions-tab control for conditions or connected workflows on a vibe block, or whether a vibe block's `ADD_RECORD` fires the Softr Apps "Add record" workflow trigger.
|
|
23
|
+
>
|
|
24
|
+
> Until someone sees those controls in Studio, start workflows from code with `navigate()` on a `TRIGGER_CUSTOM_WORKFLOW` setting (SKILL.md, NavigationAction).
|
|
25
|
+
|
|
19
26
|
Each Action's "FIELDS USED" list mirrors the aliases in your `q.select()` mapping. Actions are not a separately-managed system:
|
|
20
27
|
|
|
21
28
|
- The platform parses your block's source on every save
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "softr-vibe-coding",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.13.0",
|
|
4
4
|
"description": "Claude Code skill for generating production-ready Softr Vibe Coding blocks (JSX). Installs into ~/.claude/skills/ and auto-updates on each Claude Code session.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"softr-vibe-coding": "./bin/cli.js"
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# Checking a pushed block in a browser
|
|
2
|
+
|
|
3
|
+
How to check a deployed block's rendering and behaviour in a Softr preview with the
|
|
4
|
+
[agent-browser](https://github.com/vercel-labs/agent-browser) CLI. **Verified 2026-10-01** with
|
|
5
|
+
agent-browser v0.38.1 on macOS (Node 22) against a real Softr preview; only the commands under
|
|
6
|
+
[Untested but promising](#untested-but-promising) were not run.
|
|
7
|
+
|
|
8
|
+
## When to use it
|
|
9
|
+
|
|
10
|
+
After a push has passed the hash check in
|
|
11
|
+
[softr-mcp.md → Verifying a push](softr-mcp.md#verifying-a-push--the-deployed-source-is-the-only-proof).
|
|
12
|
+
The hash proves what Softr stored; a browser shows what it does: the layout at a given width, what
|
|
13
|
+
a control does, what a Save would send. **Not for data checks** (read records through the MCP or the
|
|
14
|
+
API), and **not for logged-in Studio or Airtable work**, which needs the user's own session and so
|
|
15
|
+
belongs to the user's own Chrome tool.
|
|
16
|
+
|
|
17
|
+
## Tool choice and why
|
|
18
|
+
|
|
19
|
+
**agent-browser**, because a check costs fewer tokens and can be made safe:
|
|
20
|
+
|
|
21
|
+
- One browser stays alive between shell commands (a daemon per `--session`), so a check is a few
|
|
22
|
+
short commands rather than one script.
|
|
23
|
+
- `eval` prints only its result. The Playwright MCP's `browser_run_code_unsafe` repeats the whole
|
|
24
|
+
script back in every result, under "### Ran Playwright code".
|
|
25
|
+
- Screenshots go to disk and cost nothing until someone opens one.
|
|
26
|
+
- It aborts requests by URL pattern and lists what a click sent: the write guard below.
|
|
27
|
+
|
|
28
|
+
An in-app or embedded browser pane stops rendering while it is hidden: IntersectionObserver and
|
|
29
|
+
`requestAnimationFrame` never fire, and screenshots time out. It can check scroll- or
|
|
30
|
+
visibility-driven behaviour only while it is visibly open.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
Check with `agent-browser --version`. If it is missing, **ask the user before installing**: it is a
|
|
35
|
+
global npm package plus a Chrome for Testing download (182 MB, about 360 MB on disk under
|
|
36
|
+
`~/.agent-browser`). On a yes:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm i -g agent-browser && agent-browser install
|
|
40
|
+
agent-browser doctor # passed every check; a headless launch took about 0.9 s
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
npm may warn `EBADENGINE`, asking for Node ≥ 24. It ran fine on Node 22: that requirement is for
|
|
44
|
+
building from source, and the CLI is a native binary. If the user declines, drive a headless Chrome
|
|
45
|
+
with Playwright, or use an in-app browser only while it is visibly open.
|
|
46
|
+
|
|
47
|
+
## Why not the skills.sh `agent-browser` skill
|
|
48
|
+
|
|
49
|
+
vercel-labs also publish an `agent-browser` skill on skills.sh. Do not install it. Its description
|
|
50
|
+
tells the agent to prefer it over every built-in browser tool, and it triggers on generic requests,
|
|
51
|
+
even Slack ones. Yet it is only a stub that loads `agent-browser skills get core` (about 38 KB, some
|
|
52
|
+
10k tokens). The CLI serves that guide on demand, matched to the installed version: run
|
|
53
|
+
`agent-browser skills get core` yourself, and only when this recipe is not enough.
|
|
54
|
+
|
|
55
|
+
## The recipe
|
|
56
|
+
|
|
57
|
+
### 1. Session, preview cookie, page
|
|
58
|
+
|
|
59
|
+
Work from a scratch directory, not the project: nothing is written to the working folder, and
|
|
60
|
+
screenshots go where you tell them.
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
ab() { agent-browser --session softr-check "$@"; } # a function, not a variable: see Gotchas
|
|
64
|
+
ab open '<previewUrl>' >/dev/null # once per session: sets the preview cookie
|
|
65
|
+
ab set viewport 1280 900
|
|
66
|
+
ab open 'https://<subdomain>.preview.softr.app/<page>?recordId=<recordId>&autoUser=true'
|
|
67
|
+
ab wait --load networkidle # works on Softr previews
|
|
68
|
+
ab wait 2500 # 2000–3000 ms more, so the block's data hooks can load
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`<previewUrl>` is what the MCP's `application_preview` returns. `open` prints the URL it opened, and
|
|
72
|
+
this one carries a sign-in token, hence `/dev/null`. The direct URL then loads the app itself, not
|
|
73
|
+
the toolbar shell that frames it, so `document` in `eval` is the app's.
|
|
74
|
+
|
|
75
|
+
### 2. Reaching into the block
|
|
76
|
+
|
|
77
|
+
A block renders inside a shadow root, which CSS selectors and `find` locators do not cross.
|
|
78
|
+
**Refs from the accessibility readout do**: `ab snapshot -i` lists the block's textboxes, buttons
|
|
79
|
+
and checkboxes as `[ref=eN]`, and `ab fill @eN '…'` and `ab click @eN` act inside the block. Grep
|
|
80
|
+
the ref out in the same shell call, so the readout never enters your context:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
REF=$(ab snapshot -i | grep -o 'textbox "Search by[^[]*\[ref=e[0-9]*' | head -1 | grep -o 'e[0-9]*$'); ab fill "@$REF" 'term'
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 3. Measuring with `eval`
|
|
87
|
+
|
|
88
|
+
`ab eval "<expr>"`, or `ab eval --stdin < check.js` for anything longer. An async IIFE is awaited,
|
|
89
|
+
and only the result is printed: return `JSON.stringify(...)` to get one JSON-encoded line. Find the
|
|
90
|
+
block's shadow root by text only that block contains:
|
|
91
|
+
|
|
92
|
+
```js
|
|
93
|
+
(async () => {
|
|
94
|
+
const root = [...document.querySelectorAll('*')].map(e => e.shadowRoot).filter(Boolean)
|
|
95
|
+
.find(s => /<text unique to the block>/.test(s.textContent));
|
|
96
|
+
if (!root) return 'block not found';
|
|
97
|
+
const r = root.querySelector('<selector>').getBoundingClientRect();
|
|
98
|
+
return JSON.stringify({ left: r.left, right: innerWidth - r.right, top: r.top, width: r.width });
|
|
99
|
+
})()
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
A date input has no ref (role `Date` in the full readout, absent from `-i`) and is React-controlled:
|
|
103
|
+
set it through the native setter, then fire both events, inside the IIFE once `root` is found.
|
|
104
|
+
|
|
105
|
+
```js
|
|
106
|
+
const i = root.querySelector('input[type="date"]');
|
|
107
|
+
const set = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set;
|
|
108
|
+
set.call(i, '2026-10-15');
|
|
109
|
+
i.dispatchEvent(new Event('input', { bubbles: true }));
|
|
110
|
+
i.dispatchEvent(new Event('change', { bubbles: true }));
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### 4. Block saves before any click, and prove it
|
|
114
|
+
|
|
115
|
+
The preview writes to the live data
|
|
116
|
+
([softr-mcp.md](softr-mcp.md#testing-as-any-app-user-without-logins--the-preview-as-switcher)), so
|
|
117
|
+
before a check clicks anything that could save:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
ab network route '*records-trigger*' --abort
|
|
121
|
+
ab eval "fetch('/records-trigger-probe-' + Date.now()).then(r => 'NOT BLOCKED ' + r.status).catch(e => 'blocked: ' + e.message)"
|
|
122
|
+
# blocked: Failed to fetch
|
|
123
|
+
ab eval "fetch('/plain-probe-' + Date.now()).then(r => 'reached ' + r.status).catch(e => 'blocked: ' + e.message)"
|
|
124
|
+
# reached 404 (the control: other requests still get through)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
A `useRecordUpdate` save goes out as a PATCH, with the record ID in the path (see Gotchas):
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
PATCH https://<subdomain>.preview.softr.app/v1/datasource/applications/<app>/pages/<page>/blocks/<block>/datasources/<ds>/records-trigger/<recordId>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Only this update endpoint is verified. Before clicking a create or delete control, learn its URL
|
|
134
|
+
with `ab network requests` where a write is harmless, never on client data, and block that pattern
|
|
135
|
+
too. Do not assume `*records-trigger*` covers it.
|
|
136
|
+
|
|
137
|
+
### 5. Click, then read what it sent
|
|
138
|
+
|
|
139
|
+
Read the record through the database API, click Save by its ref (step 2), then read the record
|
|
140
|
+
again: `updatedAt` and the field should be unchanged. The aborted request is still logged, so you
|
|
141
|
+
see the payload without it reaching the server:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
ab network requests --filter records-trigger # lists the save, with its id
|
|
145
|
+
ab network request <id> --json # method: PATCH, postData: {"context":{…},"fields":{"<fieldId>":"2026-10-15"}}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### 6. Screenshots and cleanup
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
ab screenshot ./empty-1280.png # ✓ Screenshot saved to … (--full for the whole page)
|
|
152
|
+
ab network unroute
|
|
153
|
+
ab close # ✓ Browser closed (no process left behind)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Give a path; without one, it writes to a temp directory. A saved screenshot costs no tokens until
|
|
157
|
+
someone opens it; one shown inline costs about 1.5k. Left alone, the daemon exits after an hour idle.
|
|
158
|
+
|
|
159
|
+
## Gotchas
|
|
160
|
+
|
|
161
|
+
- **zsh does not word-split.** `AB="agent-browser --session x"; $AB open …` fails with "command not
|
|
162
|
+
found": use the function. Agent shells usually keep no functions or variables between calls, so
|
|
163
|
+
define `ab`, and grep any ref, in the call that uses them; the browser lives on in the daemon.
|
|
164
|
+
- **Selectors stop at the shadow root.** `ab fill 'input[placeholder^="…"]' 'x'` gives
|
|
165
|
+
`✗ Element not found`, and `find` locators fail the same way (upstream issue
|
|
166
|
+
vercel-labs/agent-browser#1266, open since April 2026). Use refs, or `eval`.
|
|
167
|
+
- **Refs change on every page load.** Grep again after each `open` or reload.
|
|
168
|
+
- **Readouts are huge on data pages.** A table-heavy page measured 268 KB in full, 197 KB with `-c`
|
|
169
|
+
and 164 KB with `-i` (or `-i -c`), about 40k tokens: `-i` keeps table cells as context for the
|
|
170
|
+
buttons in them. A small page was 5.8 KB, 2.9 KB with `-i`. Never print one on a data page: grep
|
|
171
|
+
it, or write it to a file.
|
|
172
|
+
- **Saves are PATCH, and the record ID is in the path.** A guard on POST misses them, and so does one
|
|
173
|
+
that looks for the record ID in the body; that one once let a write through. Match the URL, never
|
|
174
|
+
the method or the body.
|
|
175
|
+
- **Plain `ab network request <id>` printed only the URL** of the blocked save. Add `--json`.
|
|
176
|
+
- **A preview serves the version it was minted on.** After every push, mint a fresh one with
|
|
177
|
+
`application_preview` and open it again before checking anything.
|
|
178
|
+
- **The preview URL is a sign-in token.** Never share it ([why](softr-mcp.md#application-management-tools)).
|
|
179
|
+
- **Attachment URLs are re-signed on every read:** compare by id, filename and size, never by URL.
|
|
180
|
+
|
|
181
|
+
## Measured
|
|
182
|
+
|
|
183
|
+
The same check, an empty-state centring check at two widths plus a screenshot, cost about 650 tokens
|
|
184
|
+
with agent-browser (2026-10-01) against about 1,540 with the Playwright MCP's run-code
|
|
185
|
+
(2026-09-30): about 58% less, mostly because Playwright repeats the script back. Learning the tool
|
|
186
|
+
in a fresh session cost about 1.6k tokens, once, without loading `skills get core`.
|
|
187
|
+
|
|
188
|
+
## Untested but promising
|
|
189
|
+
|
|
190
|
+
In agent-browser's docs, **not yet tried against a Softr preview**. Try one before relying on it:
|
|
191
|
+
|
|
192
|
+
- `ab pdf <path>`: to check a print layout ([printing.md](printing.md#7-gotchas-and-testing) has
|
|
193
|
+
the verified Playwright route).
|
|
194
|
+
- `--init-script <path>` (before the first navigation) or `ab addinitscript <js>` (at runtime): for
|
|
195
|
+
example, to stub `window.print` before load.
|
|
196
|
+
- `ab screenshot --if-changed`: skips a screenshot that matches the last one.
|
|
197
|
+
- `ab diff snapshot`: compares the current readout with the last one.
|
|
198
|
+
- `ab a11y`: the built-in axe-core accessibility audit.
|
|
199
|
+
- `--allowed-domains <list>`: restricts the session's network to the domains listed.
|
|
@@ -137,7 +137,7 @@ Value shapes per action type (full detail in SKILL.md's NavigationAction section
|
|
|
137
137
|
- `OPEN_PAGE` — `destination` (page path) + `openIn` (`"SELF"` | `"TAB"` | `"MODAL"`)
|
|
138
138
|
- `OPEN_URL` — `destination` (URL) + `openIn` (`"SELF"` | `"TAB"`)
|
|
139
139
|
- `OPEN_CHAT` — no destination (mind the data-source-context gotcha in [anti-patterns.md](anti-patterns.md))
|
|
140
|
-
- `TRIGGER_CUSTOM_WORKFLOW` — no destination; builder picks the workflow in Studio (workflow-side receiving end
|
|
140
|
+
- `TRIGGER_CUSTOM_WORKFLOW` — no destination; builder picks the workflow in Studio. Code can also run it with `navigate(setting, { recordId, datasourceId })` (documented 2026-10-05). The workflow-side receiving end is the "Run custom workflow" trigger (`SOFTR_APPS_TRIGGER_WORKFLOW`); see [softr-mcp.md](softr-mcp.md#workflows)
|
|
141
141
|
|
|
142
142
|
**[verified-undocumented] `action` is accepted as optional.** Softr's setting validator accepts an initialValue with no `action` key — `{ destination: "/", openIn: "SELF" }` alone — and Studio's own AI emits exactly that shape (verified 2026-08-31: a Studio-generated block with three action-less `useNavigationSetting` initialValues, plus three more action-less link values inside an array setting, saved and rendered its Settings pane). Corroborating in-repo evidence that not every key is mandatory: the validator's own error message for `openIn` ends in *"if provided"* (see [anti-patterns.md](anti-patterns.md#editable-settings)). Two consequences:
|
|
143
143
|
|
package/references/softr-mcp.md
CHANGED
|
@@ -47,6 +47,13 @@ On 2026-10-01 Softr renamed the workspace-server tools outside Workflows to **ar
|
|
|
47
47
|
(`application_update_pwa_settings`); the 28 Workflows tools and `get_workspace_integrations` kept their names. The
|
|
48
48
|
map below was checked against the tool lists the server delivered on 2026-09-30 (old) and 2026-10-01 (new).
|
|
49
49
|
|
|
50
|
+
**The Workflows tools have since followed** (when exactly is not known; first seen 2026-10-05). The 2026-10-05 roster delivered all 28 as `workflow_*`:
|
|
51
|
+
`workflow_create`, `workflow_get`, `workflow_list`, `workflow_publish`, `workflow_update_node_inputs`,
|
|
52
|
+
`workflow_get_node_specifications`, `workflow_list_node_types`, `workflow_test_node` and the rest,
|
|
53
|
+
i.e. area first, then the old verb and object. [Workflows](#workflows) below still lists the
|
|
54
|
+
pre-rename names. Translate them that way. That roster had no `get_workspace_integrations`;
|
|
55
|
+
`integration_list` covers it.
|
|
56
|
+
|
|
50
57
|
Most new names are the old words reordered. These are the ones you would not guess:
|
|
51
58
|
|
|
52
59
|
| Old | New |
|
|
@@ -293,6 +300,8 @@ definition says, check that `sourceCode` came back `null`.
|
|
|
293
300
|
result to a file rather than returning it inline, so compare from that file with a script, never
|
|
294
301
|
by eye.
|
|
295
302
|
|
|
303
|
+
Matching hashes prove what Softr stored, not how the block behaves: for that, check it in a fresh preview with saves blocked, per [browser-checks.md](browser-checks.md).
|
|
304
|
+
|
|
296
305
|
**Hash the exact bytes, trailing newline included.** Softr stores exactly what it receives: across
|
|
297
306
|
112 push→fetch pairs between 2026-09-09 and 2026-09-30 the fetched
|
|
298
307
|
`sourceCode` was byte- and MD5-identical to the text sent, including two pushes sent *without* a
|
|
@@ -451,31 +460,78 @@ Both edit paths recompile, so both reset Action permissions either way (Hard Con
|
|
|
451
460
|
### What the server enforces on a block's data endpoints
|
|
452
461
|
|
|
453
462
|
*Verified live 2026-09-18 (Softr Database; draft preview, "Preview as" different users, requests
|
|
454
|
-
captured from the app iframe)
|
|
463
|
+
captured from the app iframe); the block-visibility row verified 2026-10-05 (HubSpot; preview link,
|
|
464
|
+
impersonated users, direct POSTs).* A block's data lives behind per-connection endpoints —
|
|
455
465
|
`/blocks/<blockId>/datasources/<dataSourceId>/records` for lists, `/records/<id>` for one record —
|
|
456
466
|
and these are the gates that actually exist on them:
|
|
457
467
|
|
|
458
468
|
| Gate | Enforced server-side? |
|
|
459
469
|
|---|---|
|
|
460
470
|
| **Page VIEW permission** | **Yes.** A viewer who cannot view the page gets **403** ("block/action visibility rules…") from the block's datasource endpoint — crafting the request by hand does not get around it |
|
|
471
|
+
| **The block's Visibility** (`predefinedUserGroup` + `customUserGroupIds`; `vibe_coding_block_set_visibility`) | **Yes.** A viewer outside the block's group gets the same **403**, on list and by-id, even where the page lets them in. The body reads like a write error on a read: "You cannot add or edit a record because either the block/action visibility rules, user group conditions, or the user/record data in the datasource has changed." Per block: the same table on an ungated block stays open. Five code pushes left the setting intact |
|
|
461
472
|
| **The connection's Source conditions** (Source tab / `vibe_coding_block_set_data_source_record_filters`) | **Yes — and they are the only server-side ROW gate** |
|
|
462
473
|
| A `where` filter in the block's code | No — it is a request parameter the caller controls |
|
|
463
474
|
| Which fields the block *renders*, a second / conditional `q.select`, `enabled: false` on `useRecords` | No — the endpoint returns the union of the connection's read selects to anyone allowed to call it (see [multi-datasource.md](../datasources/multi-datasource.md#one-connection--one-read-payload-the-union-of-its-selects)) |
|
|
464
475
|
|
|
465
476
|
The consequence to design around: **on a page any logged-in user may view, every datasource
|
|
466
|
-
connected to its blocks is readable by any logged-in user who crafts the request** — all rows the
|
|
477
|
+
connected to its ungated blocks is readable by any logged-in user who crafts the request** — all rows the
|
|
467
478
|
Source conditions allow, all fields the block's read selects name. A per-record access check in
|
|
468
479
|
React ("is this viewer a party to this record?") shapes the UI; it is not access control. When rows
|
|
469
|
-
must be private per user, put it in the Source conditions (e.g. a logged-in-user condition
|
|
480
|
+
must be private per user, put it in the Source conditions (e.g. a logged-in-user condition; see
|
|
481
|
+
[below](#logged-in-user-values-in-source-conditions) for the two forms known to work) or on a
|
|
470
482
|
page only the right group can view. When a field must be private, a second connection of the table
|
|
471
483
|
keeps it out of every ordinary browser's payload — but not away from a crafted request by someone
|
|
472
484
|
who may view the page; for that it has to live on a page (or in a group-gated block) the viewer
|
|
473
|
-
cannot see.
|
|
474
|
-
|
|
485
|
+
cannot see. Conversely, a block gated to a staff group may read unfiltered connections for a staff
|
|
486
|
+
view: on 2026-10-05 group members got all 41 deals and 16 tickets, and everyone else got 403, with
|
|
487
|
+
the page VIEW permission at `LOGGED_IN_USERS` throughout.
|
|
475
488
|
|
|
476
489
|
This is also what makes the open-`ADD_RECORD` finding above severity-dependent on the page's VIEW
|
|
477
490
|
permission rather than uniformly critical.
|
|
478
491
|
|
|
492
|
+
#### Logged-in-user values in Source conditions
|
|
493
|
+
|
|
494
|
+
*The email token was verified on Softr Database in a production project: set in Studio's Source
|
|
495
|
+
tab and read back with `vibe_coding_block_get_settings` on 2026-09-01, then set over MCP and checked
|
|
496
|
+
against the records endpoint's totals as different users on 2026-09-18. The user-field token was
|
|
497
|
+
verified on HubSpot on 2026-10-05, the same way.* There are two forms, and **their braces differ**:
|
|
498
|
+
|
|
499
|
+
- **The email: `{USER:::EMAIL}`, with braces, only as the ENTIRE value** of an expression, e.g.
|
|
500
|
+
`{ "subject": { "field": "<fieldId>", "type": "TEXT" }, "operator": "CONTAINS", "value": ["{USER:::EMAIL}"] }`.
|
|
501
|
+
Softr substitutes it as a whole-value token, not by string interpolation. The embedded form
|
|
502
|
+
`",{USER:::EMAIL},"` returned total 0 for every user, including users who should have matched.
|
|
503
|
+
- **A users-table field: `USER:::<user field id>`, with NO braces, as the entire value** (verified
|
|
504
|
+
2026-10-05 on HubSpot). `associations.company IS_ONE_OF ["USER:::associations.company"]` on a
|
|
505
|
+
deals connection gave each user only their own companies' deals. Studio stores a picked user
|
|
506
|
+
field in this form (Leo set it in the Source tab, and it read back that way), and the same form
|
|
507
|
+
works when written with `vibe_coding_block_set_data_source_record_filters`.
|
|
508
|
+
**It fails closed:** a user whose field is empty, or who has no record in the users' data source,
|
|
509
|
+
gets 0 rows, and a by-id request for a record outside the condition returns 404.
|
|
510
|
+
- **Use AND between rules.** With one rule OR and AND behave the same, but a second rule added
|
|
511
|
+
under OR widens access (2026-10-05).
|
|
512
|
+
- **The braced user-field spellings fail.** On 2026-09-18, on Softr Database, eleven spellings were
|
|
513
|
+
tried, including `{USER:::<fieldId>}`, `{USER:<fieldId>}` and `{USER:::FIELD:<fieldId>}`. Each
|
|
514
|
+
silently matched nothing. All eleven had braces, so the braceless form is untested on Softr
|
|
515
|
+
Database, not disproved. A subject of `USER:<fieldId>`, the syntax user-group rules use, returned
|
|
516
|
+
HTTP 400 "Field not found": the user field goes in the value, never the subject. No token for a
|
|
517
|
+
user group has been found.
|
|
518
|
+
- Studio's conditional-filter UI offers the logged-in user's Email and Email-Domain, plus every
|
|
519
|
+
users-table field once users sync from a data source (documented). For a value not listed here,
|
|
520
|
+
pick it in a block's Source tab, save, and read `dataSources[].condition` back with
|
|
521
|
+
`vibe_coding_block_get_settings`. That is how the user-field form was found.
|
|
522
|
+
- **CONTAINS against a list of emails has a substring trap:** `bob@x.com` matches a field holding
|
|
523
|
+
`jbob@x.com`. Prefer IS against a single-email field. If a record must hold several emails, the
|
|
524
|
+
delimiter trick the embedded form was meant to provide does not work, so accept the trap or
|
|
525
|
+
split the data.
|
|
526
|
+
- When a row gate must follow something other than the user's email (a company, a team), compare
|
|
527
|
+
the record's field with the user's own field through `USER:::<user field id>`. Where that form is
|
|
528
|
+
untested (Softr Database so far), store an email on the record and compare it with
|
|
529
|
+
`{USER:::EMAIL}`. Staff who need every row get a group-gated block with unfiltered connections
|
|
530
|
+
([above](#what-the-server-enforces-on-a-blocks-data-endpoints)), not a wider condition.
|
|
531
|
+
|
|
532
|
+
For HubSpot specifics (association-based scoping, owner fields), see
|
|
533
|
+
[../datasources/hubspot.md](../datasources/hubspot.md#row-scoping--who-sees-which-records).
|
|
534
|
+
|
|
479
535
|
## Adopting Studio-AI-generated code
|
|
480
536
|
|
|
481
537
|
When you pull a Studio-AI-generated block via `vibe_coding_block_get_code` to adopt into a project repo as source of truth: its output renders fine but ships with predictable defects. **Functional patterns in Studio output are platform-support evidence** (it surfaces undocumented capabilities before the docs do — see SKILL.md's "Platform truth sources"); **its code hygiene is not a pattern to imitate.** Cleanup pass before committing:
|
|
@@ -554,6 +610,13 @@ passwords, no test accounts to create:
|
|
|
554
610
|
[../datasources/](../datasources/) were established — read the wire, not the rendered UI.
|
|
555
611
|
- Blocks render in shadow roots inside that iframe: read the DOM through
|
|
556
612
|
`iframe.contentDocument` and each block host's `shadowRoot`, not `document.querySelector`.
|
|
613
|
+
- **Switch user without the switcher** (verified 2026-10-05): on the preview link's origin,
|
|
614
|
+
`fetch('/studio/impersonate/<softrUserId>')` makes the preview run as that user. The id is the
|
|
615
|
+
user's Softr id from `application_list_users`. It works from a fresh preview link, so it needs no
|
|
616
|
+
Studio session in the browser.
|
|
617
|
+
- **Press the preview's own `#refresh-button` after a push.** An open preview kept serving the old
|
|
618
|
+
block version until it was pressed (verified 2026-10-05). Minting a fresh link does too (see the
|
|
619
|
+
version note above).
|
|
557
620
|
|
|
558
621
|
> **The preview is wired to the LIVE datasource.** Anything clicked there — a Save, a status
|
|
559
622
|
> change, a form submit — writes real records, as the previewed user. Keep preview sessions to
|
|
@@ -667,9 +730,19 @@ Softr Workflows are automations built from trigger + action nodes, and the MCP c
|
|
|
667
730
|
| Node management | `add_node`, `add_branch_node`, `create_branch`, `delete_node`, `duplicate_node`, `rename_node`, `reorder_node`, `reorder_multiple_nodes`, `replace_node`, `replace_trigger_node`, `update_node_inputs`, `update_node_note`, `update_node_continue_on_error`, `update_node_retry` |
|
|
668
731
|
| Discovery / testing | `list_node_types`, `get_node_specifications`, `get_dynamic_input_options`, `test_node`, `get_node_output` |
|
|
669
732
|
|
|
733
|
+
These are the names as of 2026-10-01. By 2026-10-05 the server delivered them as `workflow_*`
|
|
734
|
+
(`create_workflow` → `workflow_create`, `update_node_inputs` → `workflow_update_node_inputs`); see
|
|
735
|
+
[the rename note](#tool-names--the-2026-10-01-rename).
|
|
736
|
+
|
|
670
737
|
**The node catalog is huge** — live-enumerated 2026-08-31: **418 node types (58 triggers + 360 actions) across 56 applications.** The parts that matter most for this skill:
|
|
671
738
|
|
|
672
|
-
- **Softr-native triggers:** one-time + recurring schedules, `WEBHOOK` (inbound webhook), `SOFTR_EMAIL_RECEIVED` (inbound email! — delivery mechanism not captured), Softr Databases record events (added / updated / deleted / meets-conditions / enters-view / "run custom workflow on selected records"), Softr Apps events (Add Record form submitted, Edit Record form submitted, form submitted, user added, comment added) — and **"Run Custom Workflow action triggered"
|
|
739
|
+
- **Softr-native triggers:** one-time + recurring schedules, `WEBHOOK` (inbound webhook), `SOFTR_EMAIL_RECEIVED` (inbound email! — delivery mechanism not captured), Softr Databases record events (added / updated / deleted / meets-conditions / enters-view / "run custom workflow on selected records"), Softr Apps events (Add Record form submitted, Edit Record form submitted, form submitted, user added, comment added) — and **"Run custom workflow"** (`SOFTR_APPS_TRIGGER_WORKFLOW`; this file called it "Run Custom Workflow action triggered" until 2026-10-05, and the live label is "Run custom workflow"). This is the receiving end of the vibe block's `TRIGGER_CUSTOM_WORKFLOW` action.
|
|
740
|
+
- **From the block:** the live developer guide documents `navigate(setting, { recordId, datasourceId })` on a `TRIGGER_CUSTOM_WORKFLOW` setting, callable for example from `useRecordCreate`'s `onSuccess`, and says "the builder picks which action it points at" (documented, checked 2026-10-05). We have not yet run it end to end ourselves.
|
|
741
|
+
- **The trigger and its payload:** its only input is `corsAllowedOrigins`. Saved payloads look like `{ body: { ... }, query: {} }`, with `body` holding whatever the triggering action mapped. Native buttons can map form or record fields; the vibe path documents only `recordId` and `datasourceId`.
|
|
742
|
+
- **Who can call it:** the endpoint is browser-callable (per its spec, an empty `corsAllowedOrigins` allows any origin), so re-read the record server-side rather than trusting the body.
|
|
743
|
+
- **Wait screen:** `workflow_create` scaffolds the Show Wait Screen and End User Interactions steps for this trigger. Whether a vibe `navigate()` call shows the wait screen is undocumented; the docs describe the toggle only on native app actions.
|
|
744
|
+
|
|
745
|
+
See SKILL.md's NavigationAction action-types list.
|
|
673
746
|
- **Softr-native actions:** `BRANCH`, `FILTER`, `WAIT`, `LOOP_ACTION_GROUP` (run each list item through the same steps), `SOFTR_SEND_EMAIL`, `CALL_API` (REST), `WEBPAGE_SCRAPPER`, `PDF_TO_TEXT`, `COMPRESS_FILES` (zip + download link), `TRANSFORM_DATA`, `RESPONDED_TO_WEBHOOK` (custom HTTP response to the webhook caller); Softr DB record CRUD incl. bulk update/delete and find; Softr Apps user management (find / create / delete / deactivate / activate / invite user, send push notification).
|
|
674
747
|
- **`CUSTOM_CODE`:** runs custom **JavaScript or Python** inside a workflow.
|
|
675
748
|
- **AI actions:** Softr AI, OpenAI, Anthropic, Gemini, and Mistral each ship Write / Summarize / Categorize / Custom-prompt nodes; OpenAI adds gpt-image-2 image generation. Pinecone, Firecrawl, Replicate, and Linkup nodes exist too.
|
|
@@ -679,12 +752,15 @@ Softr Workflows are automations built from trigger + action nodes, and the MCP c
|
|
|
679
752
|
|
|
680
753
|
- Node inputs can embed **references to another node's runtime output**, a loop's current item, or named date/time tokens.
|
|
681
754
|
- **Test-first is mandated:** every testable node needs a test run before its outputs become referenceable by downstream nodes. Each node carries a `testRunMode` — `REAL_ONLY`, `MOCK_ONLY`, or `MOCK_AND_REAL` — so some nodes can only be tested against real side effects while others mock. See the test-safety rules under build-loop findings below before testing anything against a production workspace.
|
|
682
|
-
- **Workflows are workspace-
|
|
755
|
+
- **Workflows are owned by a workspace, and can now be pinned to an app** (verified 2026-10-05 from the tool definitions). `workflow_create` still requires a `workspaceId`; its optional `applicationId` "pins the workflow to it, so it is listed on that app's Workflows tab", and `workflow_list({ applicationId })` lists the workflows pinned to an app. Leave `applicationId` out for a workflow that belongs to the workspace as a whole. Pinning or not, `application_preview` / `application_publish` do not apply to workflows. Link a workflow as `https://studio.softr.io/workflow/{workflowId}`.
|
|
683
756
|
|
|
684
757
|
**Build-loop findings (verified live 2026-09-01, first end-to-end production build — 10 workflows):**
|
|
685
758
|
|
|
686
759
|
- **`create_workflow` instantiates an OLD version of the trigger node.** Immediately call `replace_trigger_node` with the **same trigger type** — the replacement lands at the current version with the current inputs. Example: `updateField` on `SOFTR_TABLES_RECORD_UPDATED` (fire only when a specific field changed) only exists at v1.2.0; the version `create_workflow` instantiates doesn't have it.
|
|
687
760
|
- **FILTER node conditions are set via `update_node_inputs` with inputName `"condition"`** — the value is an `{operator, conditions: [...]}` object. The condition is stored on the FILTER node's **outgoing path**, the same way the Studio builder wires it.
|
|
761
|
+
- **The official MCP docs disagree:** "Branch and filter conditions can't be set through MCP yet ... deciding what sends a run down each path is something you finish in the builder" (docs.softr.io/mcp/workflows, checked 2026-10-05).
|
|
762
|
+
- **What stands on each side:** our 2026-09-01 build did set them this way. The FILTER spec today declares `inputs: {}`, but live FILTER nodes still keep their condition on the outgoing path, so the empty spec does not refute the mechanism.
|
|
763
|
+
- **Until it's re-checked:** after setting a condition over MCP, read the workflow back (`workflow_get`) and confirm the path condition is there. If it isn't, finish the condition in the builder.
|
|
688
764
|
- **`LOOP_ACTION_GROUP`'s `loopVariables.items` must reference a plain array**, e.g. `$.records` — a `[*]` projection (e.g. `$.records[*].fields.X`) is rejected by the validator. Per-item references **inside** the loop use `{loopActionGroup.<id>:::loopVariables.items.fields.<fieldId>}` (use the bracket form for ids that start with a digit).
|
|
689
765
|
- **`update_node_inputs` batches validate against the STORED node state**, not the batch-in-progress — an update that depends on another update in the same batch fails validation. Split dependent updates into sequential calls.
|
|
690
766
|
- **Test-safety rules** (which `testRunMode` means what in practice):
|