softr-vibe-coding 2.13.0 → 2.13.1
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 +7 -0
- package/README.md +2 -2
- package/SKILL.md +1 -1
- package/datasources/hubspot.md +190 -60
- package/datasources/reading.md +5 -4
- package/datasources/writing.md +75 -2
- package/package.json +1 -1
- package/references/anti-patterns.md +4 -3
- package/references/helper-blocks.md +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,13 @@ 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.1] - 2026-10-05
|
|
8
|
+
- Release 2.13.1
|
|
9
|
+
- Keep HubSpot association claims at their tested scope (second review round)
|
|
10
|
+
- Record HubSpot association writes: create with id strings, update with [{ id }] replaces the list (verified 2026-10-05)
|
|
11
|
+
- Fix three stale TOC anchors in helper-blocks.md
|
|
12
|
+
- Record that no read follows a write, and HubSpot's after-write changes (verified 2026-10-05)
|
|
13
|
+
|
|
7
14
|
## [2.13.0] - 2026-10-05
|
|
8
15
|
- Release 2.13.0
|
|
9
16
|
- Record the USER::: user-field Source-condition token (verified 2026-10-05 on HubSpot)
|
package/README.md
CHANGED
|
@@ -278,7 +278,7 @@ softr-vibe-coding/
|
|
|
278
278
|
├── airtable.md # Column names, PAT vs OAuth, rate limits
|
|
279
279
|
├── google-sheets.md # Text formatting, 50-100 user cap
|
|
280
280
|
├── hubspot.md # 15 objects (listed ≠ usable), field model,
|
|
281
|
-
│ # association writes
|
|
281
|
+
│ # association writes, write behaviour, row scoping (Oct 5 2026)
|
|
282
282
|
├── notion.md # Database pages only, Relation workarounds
|
|
283
283
|
├── coda.md # API token auth, limitations
|
|
284
284
|
├── monday.md # API token, Connected Boards
|
|
@@ -325,7 +325,7 @@ The skill enforces these automatically, but good to know (verified live against
|
|
|
325
325
|
- Data hook options must be **inline object literals** — `useRecords(opts)` with a variable or wrapper fails to compile
|
|
326
326
|
- Create payloads are **flat**; update payloads are `{ recordId, fields: {...} }` — asymmetric by design
|
|
327
327
|
- `mutateAsync` is fully supported — it's the tool for sequential multi-row saves
|
|
328
|
-
- SELECT fields write by option **label string**; linked records write as arrays of record-id strings
|
|
328
|
+
- SELECT fields write by option **label string**; linked records write as arrays of record-id strings. HubSpot differs (verified Oct 2026): SELECTs take the choice id, and an association update needs `[{ id }]` objects; in the one test it replaced the ticket's whole company list (see `datasources/hubspot.md`)
|
|
329
329
|
- Every code recompile resets the block's auto-registered Actions to default permissions — tighten permissions after the last redeploy, and note there is **no cosmetic-edit exemption**: an edit that changes only a comment resets them too. **Always read the permissions back to confirm** — a create action defaults to the block's own visibility, so on a block everyone can see it comes back publicly writable, and the MCP call that re-tightens it can fail with no fallback
|
|
330
330
|
- **Blocks cannot import each other**, so two blocks that must look alike will drift — each one looks correct in isolation while the set does not. Repeated page chrome (back button, title, primary action) must sit at the same offset on every page, and a loading skeleton must track the REST state of whatever it stands in for
|
|
331
331
|
- No `import React from 'react'` — use named imports (`import { useState } from "react"`)
|
package/SKILL.md
CHANGED
|
@@ -85,7 +85,7 @@ You generate complete, production-ready Softr Vibe Coding blocks as TypeScript R
|
|
|
85
85
|
- Sub-components (FieldLabel, TextInput, ChipButton, SectionCard, etc.) defined at **module scope**, NOT inside `Block()` -- prevents inputs losing focus after one keystroke (each render creates a new component identity, React unmounts/remounts the `<input>`)
|
|
86
86
|
- When a custom DESIGN.md is in use, brand `fontFamily` (and any non-inherited brand defaults) set as an **inline style on the block's outermost wrapper** `<div>`, not relied on from `custom-code-header.html` -- Vibe Coding blocks render inside a shadow DOM and `html, body` rules don't cross that boundary. Per-element overrides (e.g. Fraunces serif on h1) still set inline at the element.
|
|
87
87
|
- `fetchNextPage` never called in the render body — only from an event handler (Load More `onClick` with `disabled={isFetching}`, the official pattern) or a guarded `useEffect` (auto-load-all)
|
|
88
|
-
- Mutations use `recordId` (not `id`) and call `refetch()` in `onSuccess`
|
|
88
|
+
- Mutations use `recordId` (not `id`) and call `refetch()` in `onSuccess` — no read follows a write otherwise (measured 2026-10-05 on a HubSpot-backed block). When the source changes other fields itself (HubSpot stamps a closing deal's close date and recalculates its probability a few seconds later), also read again a few seconds later — see [writing.md → Fields the source changes after the write](datasources/writing.md#fields-the-source-changes-after-the-write)
|
|
89
89
|
- `useRecordUpdate` payload is `{ recordId, fields: { ... } }` — nested. `useRecordCreate` payload is **flat** (no `fields` wrapper). The two shapes are asymmetric by design (verified live 2026-08-25)
|
|
90
90
|
- Sequential multi-row saves use `await hook.mutateAsync(...)` per row, in order, with stop-on-failure + retry state — `mutateAsync` is fully supported on the current platform (verified 2026-08-25; the old ".mutate() only" Action-parser rule is gone — see [datasources/writing.md](datasources/writing.md)). Independent writes to **different tables** may run in parallel via `Promise.all`; drag/reassign UIs should be optimistic with an Undo toast — see [writing.md → Parallel writes across tables](datasources/writing.md#parallel-writes-across-tables-the-one-sanctioned-parallelism)
|
|
91
91
|
- No hardcoded domains in links -- use relative paths (`/page?recordId=...`); same-page anchors written relative too (`/#section`)
|
package/datasources/hubspot.md
CHANGED
|
@@ -2,10 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
*Rewritten 2026-10-05 from a read-only verification run: five investigations and four adversarial
|
|
4
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
docs. Labels used below: **verified live** = observed that day through the Softr or HubSpot MCP,
|
|
6
|
+
in the browser's network log, or in a test block's own output in the preview; **documented** =
|
|
7
|
+
Softr or HubSpot docs say so; **inferred** = reasoned, not observed; **unverified** = nobody has
|
|
8
|
+
tested it. That run made no writes. Write tests followed that day, all in Studio preview
|
|
9
|
+
("Preview as": the builder's session acting as the chosen user, not a real login). In the
|
|
10
|
+
morning a test block created a ticket with company and contact links and then re-linked it. In
|
|
11
|
+
the afternoon the Accounts block changed the stage of that ticket and of one test deal, and a
|
|
12
|
+
client's replay of one of those requests was refused. On the write side, only what those tests
|
|
13
|
+
covered is verified live.*
|
|
9
14
|
|
|
10
15
|
## Overview
|
|
11
16
|
|
|
@@ -42,7 +47,7 @@ list had 7 before). The live integration returned **14 object ids**, i.e. all ex
|
|
|
42
47
|
which that portal did not have (verified live 2026-10-05).
|
|
43
48
|
|
|
44
49
|
**Listed does not mean usable, and usable does not mean writable.** Softr publishes no per-object
|
|
45
|
-
write matrix, and
|
|
50
|
+
write matrix, and vibe-block writes have been tested on tickets and deals only (2026-10-05).
|
|
46
51
|
|
|
47
52
|
| Object | Id | Primary field | What to know |
|
|
48
53
|
|---|---|---|---|
|
|
@@ -51,7 +56,7 @@ write matrix, and no vibe-block write to any HubSpot object has been tested.
|
|
|
51
56
|
| Deals | `deals` | `dealname` | HubSpot Free. Stage ids are portal-specific |
|
|
52
57
|
| Tickets | `tickets` | `subject` | HubSpot Free. A create needs `subject` and `hs_pipeline_stage` ([Writing](#writing)) |
|
|
53
58
|
| 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
|
|
59
|
+
| Notes | `notes` | `hs_body_preview` | A create needs `hs_timestamp`. HubSpot says a note should be associated with at least one record. Links can be written on a ticket create ([Association writes](#association-writes)); a note create with links is untested |
|
|
55
60
|
| Leads, Listings, Appointments | `leads`, `listings`, `appointments` | — | Listed live; not examined in the 2026-10-05 run |
|
|
56
61
|
| 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
62
|
| Products | `products` | `name` | Catalog entries. Links go only to deals and to other products, none to companies or contacts |
|
|
@@ -87,8 +92,14 @@ unless marked otherwise.*
|
|
|
87
92
|
|
|
88
93
|
Read them as you would any linked field: an association can arrive as a single `{ id, label }`
|
|
89
94
|
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)).
|
|
91
|
-
|
|
95
|
+
[reading.md](reading.md#filtering-by-a-linked-record-server-side)). In a vibe hook (verified
|
|
96
|
+
live 2026-10-05 on tickets), `associations.company` and `associations.contact` read as arrays
|
|
97
|
+
of `{ id, label }`, usually with the record's name as the label (one re-read shortly after a
|
|
98
|
+
link update showed the company's id instead; see [Association writes](#association-writes)).
|
|
99
|
+
The primary-company link `associations.ticket_to_company` read as one `{ id, label }` object,
|
|
100
|
+
or `null` (seen once, in a re-read shortly after a link update, while a company was still
|
|
101
|
+
linked; HubSpot's primary label was not checked). A Softr workflow's Find record output showed
|
|
102
|
+
the single-object case too.
|
|
92
103
|
- **SELECT fields carry id→label choices** in `options.choices`, and HubSpot ids are not labels.
|
|
93
104
|
- **Deals:** stage ids are portal-specific. The live portal had `6183367908` = Initial Contact
|
|
94
105
|
… `closedwon` = Closed Won, in pipeline `default` ("Sales Pipeline").
|
|
@@ -121,58 +132,158 @@ unless marked otherwise.*
|
|
|
121
132
|
|
|
122
133
|
| Field kind | Writable? | Evidence |
|
|
123
134
|
|---|---|---|
|
|
124
|
-
| Default properties | Yes |
|
|
135
|
+
| Default properties | Yes | **Verified live** 2026-10-05: `dealstage` and `hs_pipeline_stage` with `useRecordUpdate`, and `subject`, `content`, `hs_pipeline` and `hs_pipeline_stage` on a ticket create with `useRecordCreate`; the rest documented (Softr's HubSpot page) |
|
|
125
136
|
| Custom properties | Yes | Documented; untested from a vibe block |
|
|
126
137
|
| Softr computed fields (Calculation, Rollup, Count, Formula) | Read-only | Documented. These are the **only** fields Softr's page lists as read-only |
|
|
127
138
|
| 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.*`) | **
|
|
139
|
+
| Associations (`associations.*`) | Yes for a ticket's company and contact links; an update replaced the company list | **Verified live** 2026-10-05 on one ticket's `associations.company` and `associations.contact` ([below](#association-writes)); other links, including the primary company, untested |
|
|
129
140
|
| `hubspot_owner_email` / `hubspot_owner_name` | Don't | Softr-derived; write `hubspot_owner_id` instead (inferred) |
|
|
130
141
|
|
|
131
142
|
- **Ticket create:** HubSpot requires `subject` and `hs_pipeline_stage`. `hs_pipeline` is optional
|
|
132
143
|
when the portal has one ticket pipeline, because the default is used (documented, HubSpot API).
|
|
133
144
|
**Note create:** `hs_timestamp` is required (documented).
|
|
134
|
-
- **
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
145
|
+
- **Write a SELECT by its choice id** (verified live 2026-10-05). `fields: { stage: "closedwon" }`
|
|
146
|
+
on `dealstage` and `fields: { status: "1" }` on `hs_pipeline_stage` both saved, and so did a
|
|
147
|
+
ticket create with `hs_pipeline: "0"` and `hs_pipeline_stage: "1"`. The id and the
|
|
148
|
+
label differ (`'1'` vs `'New'`, `'6183367908'` vs `'Initial Contact'`). Softr Database is the
|
|
149
|
+
other way round: there the **label** is written
|
|
150
|
+
([writing.md](writing.md#dropdown--single-select-softr-database)). Whether HubSpot also accepts
|
|
151
|
+
a label is untested.
|
|
152
|
+
- **What goes over the wire** (verified live 2026-10-05, ticket stage write):
|
|
153
|
+
- The write is `PATCH …/blocks/<block>/datasources/<connection id>/records-trigger/<recordId>`
|
|
154
|
+
with the body `{"context":{…},"fields":{"hs_pipeline_stage":"1"}}`. Fields are keyed by
|
|
155
|
+
HubSpot property id, and the value is a plain string.
|
|
156
|
+
- The response holds only the written field, in its read shape:
|
|
157
|
+
`{"record":{"id":"…","fields":{"hs_pipeline_stage":{"id":"1","label":"New"}}},"triggerResponse":null}`.
|
|
158
|
+
- The connection id in the write URL is internal: the deals connection's id changed when the
|
|
159
|
+
block was recompiled. Reads use the alias (`…/datasources/tickets/records`). This only
|
|
160
|
+
matters when reading a network log.
|
|
161
|
+
- **The write endpoint enforces visibility** (verified live 2026-10-05). A client who replayed an
|
|
162
|
+
account manager's PATCH got 403. The block and its actions were both limited to account
|
|
163
|
+
managers, so the test did not show which of the two rules refused it. Every code push resets
|
|
164
|
+
action visibility (Hard Constraint 21 in SKILL.md), so re-lock it and read it back after each push.
|
|
165
|
+
|
|
166
|
+
### What HubSpot changes after a write
|
|
167
|
+
|
|
168
|
+
*Verified live 2026-10-05 on one test ticket and one test deal. The writes came from a vibe
|
|
169
|
+
block in preview; the results were read back through the HubSpot MCP and the block's own list
|
|
170
|
+
reads.*
|
|
171
|
+
|
|
172
|
+
- **A deal's close date is overwritten when it closes, won or lost.** Entering `closedwon` or
|
|
173
|
+
`closedlost` sets `closedate` to the moment of the write: `closedwon` replaced a planned date
|
|
174
|
+
four months away, and `closedlost` later did the same. Reopening the deal, or undoing the
|
|
175
|
+
change, kept the new date; the old one had to be restored by hand. Say so in the confirm step
|
|
176
|
+
before a block closes a deal.
|
|
177
|
+
- **A ticket's close date is cleared when it reopens.** Entering the closed status set
|
|
178
|
+
`closed_date` and `time_to_close`. Moving the ticket back to an open status cleared both.
|
|
179
|
+
- **Probability and weighted amount lag behind the stage.** HubSpot recalculates
|
|
180
|
+
`hs_deal_stage_probability` and `hs_projected_amount` after the write (table below).
|
|
181
|
+
- **HubSpot modifies a ticket again about 10 s after a stage write.** `hs_lastmodifieddate`
|
|
182
|
+
moved again 9 to 11 s after each of the three stage writes where it was checked.
|
|
183
|
+
- **Stage writes leave associations alone.** They do leave a permanent stage history
|
|
184
|
+
(`hs_v2_date_entered_*` / `hs_v2_date_exited_*`), even when the value is put back.
|
|
185
|
+
|
|
186
|
+
What the block's own list reads returned for the deal (Closed Lost, then Undo 5 s later):
|
|
187
|
+
|
|
188
|
+
| Read of `deals` | `dealstage`, `closedate` | `hs_deal_stage_probability`, `hs_projected_amount` |
|
|
189
|
+
|---|---|---|
|
|
190
|
+
| +4 s after Closed Lost | `closedlost`, the new date | 0.1 and 1,980: the values from before the write |
|
|
191
|
+
| +4 s after the Undo | Initial Contact, the new date kept | 0 and 0: Closed Lost's values |
|
|
192
|
+
| +12 s after the Undo | Initial Contact, the new date kept | 0.1 and 1,980: correct |
|
|
193
|
+
|
|
194
|
+
So the stage and the close date were readable within 4 s, while the computed fields were still
|
|
195
|
+
one write behind at that point. No read follows a write unless the block asks. A block that
|
|
196
|
+
shows any of these fields should, on top of the `refetch()` in `onSuccess`, read the table again
|
|
197
|
+
about 4 s and 12 s after each successful save
|
|
198
|
+
([pattern](writing.md#fields-the-source-changes-after-the-write)). A read right after the write
|
|
199
|
+
was not measured.
|
|
200
|
+
|
|
201
|
+
### Association writes
|
|
202
|
+
|
|
203
|
+
*Verified live 2026-10-05 from a test block in Studio preview, "Preview as" Softr's built-in Test
|
|
204
|
+
User (the builder's session acting as that user, who had no HubSpot record): one ticket, one
|
|
205
|
+
company and one contact. HubSpot's MCP confirmed the links, the company count and the source;
|
|
206
|
+
the primary-company link and the link labels were seen only in the block's own reads. Not
|
|
207
|
+
tested: a create with `[{ id }]` objects, objects other than tickets, writing the primary-company
|
|
208
|
+
link directly, and a real client login on the published app.*
|
|
209
|
+
|
|
210
|
+
To Softr's action parser the association fields are ordinary fields. With `associations.company`
|
|
211
|
+
and `associations.contact` in the hooks' `fields:` selects, the derived ADD_RECORD and
|
|
212
|
+
UPDATE_RECORD actions listed both ("Associated Company", "Associated Contact"), and both hooks
|
|
213
|
+
were enabled.
|
|
214
|
+
|
|
215
|
+
**Create: link with an array of id strings.** This worked:
|
|
216
|
+
|
|
217
|
+
```tsx
|
|
218
|
+
const ticketCreateFields = q.select({
|
|
219
|
+
subject: "subject",
|
|
220
|
+
pipeline: "hs_pipeline",
|
|
221
|
+
stage: "hs_pipeline_stage",
|
|
222
|
+
company: "associations.company",
|
|
223
|
+
contact: "associations.contact",
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Inside the block: const createTicket = useRecordCreate({ from: ds.tickets, fields: ticketCreateFields });
|
|
227
|
+
await createTicket.mutateAsync({
|
|
228
|
+
subject,
|
|
229
|
+
pipeline: "0", // choice ids, not labels
|
|
230
|
+
stage: "1",
|
|
231
|
+
company: [companyId], // an array of id strings, as on Softr Database
|
|
232
|
+
contact: [contactId],
|
|
233
|
+
});
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
- **The ticket was linked to the written company and contact** (confirmed in HubSpot). In Softr's
|
|
237
|
+
read, the written company was also the primary company (`associations.ticket_to_company`);
|
|
238
|
+
whether Softr's write set that or HubSpot did is not known.
|
|
239
|
+
- **An extra company link appeared.** The ticket was also linked to the contact's other company,
|
|
240
|
+
which was also that contact's primary company. The block wrote only the one company, so HubSpot
|
|
241
|
+
most likely added it (inferred), perhaps through a portal setting (unchecked). Whether it adds
|
|
242
|
+
every company of the contact or only the primary one is open. Expect extra company links on a
|
|
243
|
+
ticket created with a contact (seen once).
|
|
244
|
+
- **Link labels are not always names.** The create's result labelled both links with their ids
|
|
245
|
+
(`{ "id": "450812074179", "label": "450812074179" }`), and the update's result did the same for
|
|
246
|
+
the company. A re-read after the create had the names. A re-read about 15 s after the update
|
|
247
|
+
still labelled the company with its id while the contact had its name; a later read had the
|
|
248
|
+
name. When a label equals its id, take the name from the companies or contacts the block
|
|
249
|
+
already reads, or show the id.
|
|
250
|
+
- **HubSpot records the source** as `INTEGRATION`, detail "Softr".
|
|
251
|
+
|
|
252
|
+
**Update: `[{ id }]` objects, and the update replaced the company list.**
|
|
253
|
+
|
|
254
|
+
- `useRecordUpdate` with `company` and `contact` both as `["<id>"]` failed with **500**
|
|
255
|
+
(`Failed to update record: 500`) and, in the block's re-read, changed nothing, although the
|
|
256
|
+
create had accepted that shape.
|
|
257
|
+
- `company: [{ id }]` and `contact: [{ id }]` worked, and the company list was **replaced**: the
|
|
258
|
+
extra company was removed (HubSpot's company count went from 2 to 1). In the block's re-read
|
|
259
|
+
about 15 s later, `associations.ticket_to_company` was `null` although the written company stayed
|
|
260
|
+
linked, so expect the primary-company link to be lost (not checked in HubSpot). The contact list
|
|
261
|
+
held only the written contact before and after, so whether contact links are replaced too could
|
|
262
|
+
not be seen (likely; inferred).
|
|
263
|
+
- So, with the two shapes tried, an update could not add one link on its own: `["<id>"]` failed
|
|
264
|
+
and `[{ id }]` replaced the list. A block can write the whole list back with the new id added
|
|
265
|
+
(untested; it may lose the primary label, as the test's update did), or leave it to the
|
|
266
|
+
workflow route below.
|
|
267
|
+
- An update whose `fields:` select held no association fields left the links alone: later stage
|
|
268
|
+
writes on this ticket came from another block whose update select held only the stage, and the
|
|
269
|
+
ticket kept its company. Whether leaving them out of the payload is enough when they are in the
|
|
270
|
+
select is untested, so keep association fields out of any update select that is not meant to
|
|
271
|
+
write them.
|
|
272
|
+
|
|
273
|
+
**Nothing checked the link ids.** The create linked the ticket to a company and a contact that had
|
|
274
|
+
nothing to do with the Test User who sent it. In this test the tickets connection had no Source
|
|
275
|
+
condition and the create action had no record condition. Whether a Source condition, an action's
|
|
276
|
+
record condition or a real client session checks the links on a create or an update is untested.
|
|
277
|
+
The request comes from the browser, so treat link ids as client input: a client could probably
|
|
278
|
+
link a new ticket to another company, and that company's portal would then show it (inferred).
|
|
279
|
+
For client-facing creates, set or check the links server-side with the workflow route below.
|
|
280
|
+
|
|
281
|
+
*History: this page called association writes "read-only" until 2026-10-05 (no source), then
|
|
282
|
+
"unverified" until the test above.*
|
|
283
|
+
|
|
284
|
+
**Workflow route (documented; not run in these tests): a Softr workflow calls HubSpot's
|
|
285
|
+
associations API.** Use it to add a link without replacing the list, to set the primary label, or
|
|
286
|
+
to set a client's links server-side.
|
|
176
287
|
|
|
177
288
|
- **The step:** a **Run custom code** step (`CUSTOM_CODE` v1.2.0) with the HubSpot integration
|
|
178
289
|
attached. `fetch()` calls to `api.hubapi.com` then carry that integration's credentials, with no
|
|
@@ -248,6 +359,14 @@ page ([details](multi-datasource.md#one-connection--one-read-payload-the-union-o
|
|
|
248
359
|
written by the browser, so a determined user could tamper with it on create. That is acceptable
|
|
249
360
|
for scoping a demo; production needs a server-side source of identity. The user's company
|
|
250
361
|
association above lives in HubSpot instead, out of reach of a block with no contact write action.
|
|
362
|
+
Association fields can be written like other fields ([Association writes](#association-writes),
|
|
363
|
+
verified on tickets), so a contacts write action a client can reach could let them rewrite their
|
|
364
|
+
own company links and widen their own scope (inferred; contact association writes untested).
|
|
365
|
+
Keep `associations.company` out of any such action.
|
|
366
|
+
- **Writes are a separate question.** Whether a Source condition limits what a create or an
|
|
367
|
+
update may write, such as the link ids on a new ticket, is untested. A create on a connection
|
|
368
|
+
without one, run in preview, accepted links to a company and a contact unrelated to the user
|
|
369
|
+
([Association writes](#association-writes)).
|
|
251
370
|
- **Owner scoping.** `hubspot_owner_id` is the real property, but on a contact it names that
|
|
252
371
|
contact's owner, not the contact. Scoping "my deals" for an account manager would need a custom
|
|
253
372
|
contact property holding their own owner id, read through `USER:::<field id>` (untested). For a
|
|
@@ -280,8 +399,11 @@ page ([details](multi-datasource.md#one-connection--one-read-payload-the-union-o
|
|
|
280
399
|
hook calls into HubSpot calls is not documented. If list reads go through search, several
|
|
281
400
|
HubSpot connections on one page, times concurrent users, can reach that limit (inferred).
|
|
282
401
|
- **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
|
|
284
|
-
|
|
402
|
+
`refetch()` right after a mutation may therefore still return the old value (inferred, not
|
|
403
|
+
measured). A list read at +4 s had the written stage, while HubSpot's computed fields were
|
|
404
|
+
still stale at +4 s and correct by +12 s (verified live 2026-10-05,
|
|
405
|
+
[details](#what-hubspot-changes-after-a-write)). Show the written value optimistically and
|
|
406
|
+
read again later.
|
|
285
407
|
- Softr also caches data-source reads ("short-term" on one docs page, "24 hour" on another; scope
|
|
286
408
|
unspecified). See [overview.md](overview.md).
|
|
287
409
|
|
|
@@ -358,7 +480,9 @@ page ([details](multi-datasource.md#one-connection--one-read-payload-the-union-o
|
|
|
358
480
|
either.
|
|
359
481
|
- Time a real run before promising anything.
|
|
360
482
|
- **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).
|
|
483
|
+
HubSpot's internal recalculations and on Softr's own writes too (inferred). HubSpot does modify
|
|
484
|
+
a record again about 10 s after a stage write (measured on a ticket, 2026-10-05), so one stage
|
|
485
|
+
change from a block may fire it twice (inferred; the trigger was not run). Build two things in
|
|
362
486
|
from the start:
|
|
363
487
|
- **A stage filter.** Note that a filter on the *current* stage alone fires again on every later
|
|
364
488
|
edit to a ticket already in that stage.
|
|
@@ -381,11 +505,17 @@ page ([details](multi-datasource.md#one-connection--one-read-payload-the-union-o
|
|
|
381
505
|
|
|
382
506
|
- **Listed is not usable.** Projects needs activation, Subscriptions are read-only, Invoices and
|
|
383
507
|
Subscriptions need Commerce Hub, Line Items need a parent, Custom Objects need HubSpot Enterprise.
|
|
384
|
-
- **Association
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
508
|
+
- **Association links: create with `["<id>"]`, update with `[{ id }]`, and an update replaced the
|
|
509
|
+
ticket's whole company list** (verified live 2026-10-05, one ticket); Softr then read no primary
|
|
510
|
+
company (not checked in HubSpot). A ticket created with a contact may also get another of that
|
|
511
|
+
contact's companies. Treat link ids as client input.
|
|
512
|
+
- **SELECT ids ≠ labels, and writes take the id** (verified live 2026-10-05; label untested).
|
|
513
|
+
Take display labels from the schema's choices (`useFieldOptions` should serve them, but that
|
|
514
|
+
is untested on HubSpot); don't hardcode stage ids across portals.
|
|
515
|
+
- **Closing a deal overwrites its close date, and reopening keeps the new one.** Reopening a
|
|
516
|
+
ticket clears its close date. See [What HubSpot changes after a write](#what-hubspot-changes-after-a-write).
|
|
517
|
+
- **After a save, read again at about 4 s and 12 s** on top of the usual `refetch()`. No read
|
|
518
|
+
follows a write on its own, and HubSpot's computed fields arrive late.
|
|
389
519
|
- **The owner email and name fields are Softr's, not HubSpot's.** Filter and write
|
|
390
520
|
`hubspot_owner_id`.
|
|
391
521
|
- **Only part of each object's properties is exposed.** Check a property is in the field list
|
package/datasources/reading.md
CHANGED
|
@@ -201,9 +201,10 @@ var statusOptions = useFieldOptions({
|
|
|
201
201
|
|
|
202
202
|
// statusOptions → { options: [...], isLoading: bool }
|
|
203
203
|
// statusOptions.options → [{ id: "sel...", label: "Active", color: "greenLight1" }, ...]
|
|
204
|
-
// id — stable option id (use as a React key; NOT needed in mutate payloads
|
|
205
|
-
// fields write by LABEL string
|
|
206
|
-
//
|
|
204
|
+
// id — stable option id (use as a React key; NOT needed in mutate payloads on Softr
|
|
205
|
+
// Database — SELECT fields write by LABEL string there, verified 2026-08-25.
|
|
206
|
+
// HubSpot is the exception: write the id, verified 2026-10-05)
|
|
207
|
+
// label — display string AND the value to write in mutate payloads (not on HubSpot)
|
|
207
208
|
// color — Airtable swatch color name (optional; handy for tinting chips)
|
|
208
209
|
```
|
|
209
210
|
|
|
@@ -219,7 +220,7 @@ same `select` object can be shared by both. Reuse one `select` for many fields a
|
|
|
219
220
|
|
|
220
221
|
**When to use this vs. hardcoding:**
|
|
221
222
|
|
|
222
|
-
- **Use `useFieldOptions`** when option labels could change post-deploy — selects with rapidly-evolving lists, user-editable choices, or any case where re-pasting blocks for an option rename is annoying. Since SELECT fields write by label (verified 2026-08-25), live options also keep write payloads rename-proof: render and write `option.label`. Hardcoded labels remain fine as a display-only loading fallback while the live options fetch. Cross-table case: to render a select field from table B inside a block bound to table A (e.g. an intake form bound to Jobs that needs the Wigs `Color` options), put the `useRecords` + `useFieldOptions` in a hidden helper block bound to table B and publish the options to a `window` global (see [helper-blocks.md](../references/helper-blocks.md)).
|
|
223
|
+
- **Use `useFieldOptions`** when option labels could change post-deploy — selects with rapidly-evolving lists, user-editable choices, or any case where re-pasting blocks for an option rename is annoying. Since SELECT fields write by label on Softr Database (verified 2026-08-25), live options also keep write payloads rename-proof: render and write `option.label`. HubSpot writes the choice id instead (verified 2026-10-05; see [hubspot.md](hubspot.md#writing)). Hardcoded labels remain fine as a display-only loading fallback while the live options fetch. Cross-table case: to render a select field from table B inside a block bound to table A (e.g. an intake form bound to Jobs that needs the Wigs `Color` options), put the `useRecords` + `useFieldOptions` in a hidden helper block bound to table B and publish the options to a `window` global (see [helper-blocks.md](../references/helper-blocks.md)).
|
|
223
224
|
- **Hardcode** when the option set is stable and frequently referenced (e.g. a status enum that drives a state machine), so the label vocabulary lives in source and rename-safety is enforced by greppable constants. A robust middle ground: prefer the live options, fall back to a hardcoded list per field so the UI still renders if the helper hasn't published yet.
|
|
224
225
|
|
|
225
226
|
`useFieldOptions` is the read-side equivalent of using `useLinkedRecords` for foreign records — it abstracts away the field's option store. Items are shaped `{ id, label, color }` (note: `label`, not `title` like `useLinkedRecords`).
|
package/datasources/writing.md
CHANGED
|
@@ -128,6 +128,20 @@ updateRecord.mutate({
|
|
|
128
128
|
});
|
|
129
129
|
```
|
|
130
130
|
|
|
131
|
+
**No read follows a write** (measured live 2026-10-05 on a HubSpot-backed block). The network
|
|
132
|
+
log showed the PATCH and then no read of the table until the block's own `refetch()`. The hook
|
|
133
|
+
resolves to `{ id, fields }`: the PATCH response's record mapped through the hook's `fields:`
|
|
134
|
+
select (seen in the compiled block bundle), and that response held only the written field.
|
|
135
|
+
Nothing HubSpot-specific was seen in the hook's client code, so expect the same on other
|
|
136
|
+
sources (inferred, untested elsewhere). So:
|
|
137
|
+
|
|
138
|
+
- Call `refetch()` on every query that shows the written table.
|
|
139
|
+
- If the source changes other fields itself with a delay (a HubSpot deal's probability, for
|
|
140
|
+
example), a `refetch()` in `onSuccess` can still return them old (inferred from a read at
|
|
141
|
+
+4 s; a read right after the write was not measured). Read the table again a few seconds
|
|
142
|
+
later as well; see
|
|
143
|
+
[Fields the source changes after the write](#fields-the-source-changes-after-the-write).
|
|
144
|
+
|
|
131
145
|
#### CRITICAL: The `useRecordUpdate` payload shape (and the retired `.mutate()`-only rule)
|
|
132
146
|
|
|
133
147
|
**Payload must be `{ recordId, fields: {...} }` — not flat.** Field values must be nested inside a `fields: {...}` object. The flat form (`mutate({ recordId, status: "active" })`) can succeed at runtime, but Softr's Action parser doesn't see field references inside it, so no Update Action is derived — `enabled` stays `false`, the UI gated on it silently does nothing, and Studio's Actions tab shows "No actions used in this block yet":
|
|
@@ -284,13 +298,62 @@ completes reads as broken. The pattern (verified live 2026-08-31 on a custom Kan
|
|
|
284
298
|
3. **Revert on failure**: delete the override + `refetch()` → the card snaps back, with an
|
|
285
299
|
error toast.
|
|
286
300
|
4. **Clear on convergence**: an effect compares each override against fresh server data and
|
|
287
|
-
deletes it once they match — never clear on a timer.
|
|
301
|
+
deletes it once they match — never clear on a timer. Don't wait for fresh data to arrive
|
|
302
|
+
on its own: no read followed a write in the network log ([useRecordUpdate](#userecordupdate)),
|
|
303
|
+
so without a `refetch()` the override may never converge.
|
|
288
304
|
5. **Undo**: snapshot the *server* state before mutating (previous lead, each row's previous
|
|
289
305
|
value — per-row, since a batch may have had mixed values), and offer
|
|
290
306
|
`toast.success(msg, { duration: 8000, action: { label: "Undo", onClick: restore } })`.
|
|
291
307
|
The restore is just another optimistic move driven by the snapshot. Snapshot before the
|
|
292
308
|
write, not from the UI — the UI may already be showing an optimistic override.
|
|
293
309
|
|
|
310
|
+
### Fields the source changes after the write
|
|
311
|
+
|
|
312
|
+
Some sources change other fields of a record when one is written. HubSpot stamps a deal's close
|
|
313
|
+
date when it closes and recalculates its probability and weighted amount a few seconds later. A
|
|
314
|
+
formula or rollup that depends on the written field recalculates on any source (inferred). No
|
|
315
|
+
read follows a write, and a source that applies its own changes with a delay can still serve the
|
|
316
|
+
old values to a `refetch()` in `onSuccess` (inferred). So read the written table again once the
|
|
317
|
+
source has settled (verified live 2026-10-05 on HubSpot deals):
|
|
318
|
+
|
|
319
|
+
```tsx
|
|
320
|
+
// After a save the source changes some fields itself, a few seconds later, so the written
|
|
321
|
+
// table is read again twice. The timers are cleared if the block unmounts first.
|
|
322
|
+
const followUps = useRef<number[]>([]);
|
|
323
|
+
useEffect(() => () => followUps.current.forEach((t) => window.clearTimeout(t)), []);
|
|
324
|
+
|
|
325
|
+
async function saveStage(id: string, stage: string) {
|
|
326
|
+
if (!updateDeal.enabled) return;
|
|
327
|
+
try {
|
|
328
|
+
await updateDeal.mutateAsync({ recordId: id, fields: { stage } });
|
|
329
|
+
} catch {
|
|
330
|
+
toast.error("We couldn't save the change. Try again.");
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
void dealsQ.refetch();
|
|
334
|
+
followUps.current = followUps.current.concat(
|
|
335
|
+
[4000, 12000].map((ms) => window.setTimeout(() => void dealsQ.refetch(), ms)),
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
- **The delays depend on the source.** On HubSpot the read at +4 s already had the new stage
|
|
341
|
+
and close date, but the probability and weighted amount still held the previous write's
|
|
342
|
+
values. The read at +12 s had everything. HubSpot's own follow-up change landed 9 to 11 s
|
|
343
|
+
after a ticket write, so +12 s leaves about a second of margin: go later rather than earlier.
|
|
344
|
+
Measurements and the list of what HubSpot changes:
|
|
345
|
+
[hubspot.md](hubspot.md#what-hubspot-changes-after-a-write).
|
|
346
|
+
- **Keep the immediate `refetch()` too.** A read right after the write was not measured.
|
|
347
|
+
HubSpot documents that changes take "a few moments" to reach its search, but whether Softr's
|
|
348
|
+
list reads use that search is not documented. With an optimistic override the immediate read
|
|
349
|
+
may add little; it is cheap and keeps the `refetch()`-in-`onSuccess` rule whole.
|
|
350
|
+
- **Show the known outcome at once.** When you know what the source will set (today's date as
|
|
351
|
+
a closing deal's close date), show it in the override and let the re-read replace it.
|
|
352
|
+
- **Only where it's needed.** Add the delayed reads when the block shows a field the source
|
|
353
|
+
sets or computes. A plain field the user wrote needs nothing more than the usual `refetch()`.
|
|
354
|
+
|
|
355
|
+
## File Uploads
|
|
356
|
+
|
|
294
357
|
```jsx
|
|
295
358
|
import { useUpload } from "@/lib/datasource";
|
|
296
359
|
|
|
@@ -369,6 +432,12 @@ parentAccount: "RECORD_ID_1"
|
|
|
369
432
|
string-array shape is the verified current form on Softr Database; if a linked-record write
|
|
370
433
|
fails on an Airtable-backed block, try the `[{ id }]` object shape before deeper debugging.
|
|
371
434
|
|
|
435
|
+
**HubSpot associations: the string array worked on a create, but an update needs `[{ id }]`
|
|
436
|
+
objects** (verified live 2026-10-05, one ticket). A create with `company: ["<id>"]` linked the
|
|
437
|
+
ticket; a create with `[{ id }]` is untested. An update with string arrays failed with 500; with
|
|
438
|
+
`[{ id }]` it worked and **replaced** the ticket's company list. See
|
|
439
|
+
[hubspot.md](hubspot.md#association-writes).
|
|
440
|
+
|
|
372
441
|
### Linked-record write traps (verified live 2026-08-26)
|
|
373
442
|
|
|
374
443
|
Four Softr Database behaviors proven by direct experiment on a live production build — a
|
|
@@ -438,6 +507,10 @@ keep vocabularies as greppable constants, or fetch them live with `useFieldOptio
|
|
|
438
507
|
[reading.md](reading.md#usefieldoptions----fetch-singlemulti-select-choices)) and write
|
|
439
508
|
`option.label`.
|
|
440
509
|
|
|
510
|
+
**On HubSpot, write the choice id** (`"closedwon"`, `"1"`; verified live 2026-10-05 on
|
|
511
|
+
`dealstage` and `hs_pipeline_stage`). Whether HubSpot also accepts the label is untested, so
|
|
512
|
+
don't rely on it; see [hubspot.md](hubspot.md#writing).
|
|
513
|
+
|
|
441
514
|
**Legacy note.** Until mid-2026 this skill documented the opposite — write the option UUID,
|
|
442
515
|
labels rejected (verified April 2026 on the then-current platform). If a label write is
|
|
443
516
|
rejected on an old app, the UUID form is the thing to try; on the current platform it is not
|
|
@@ -445,7 +518,7 @@ needed.
|
|
|
445
518
|
|
|
446
519
|
### Linked Record
|
|
447
520
|
|
|
448
|
-
Array of record-id **strings** (`["RECORD_ID"]`) on Softr Database (verified 2026-08-25); the legacy / Airtable fallback shape is `[{ id }]` objects. See "Linked Record Format for Mutations" above.
|
|
521
|
+
Array of record-id **strings** (`["RECORD_ID"]`) on Softr Database (verified 2026-08-25); the legacy / Airtable fallback shape is `[{ id }]` objects. HubSpot associations: the string array worked on create; an update needs `[{ id }]` and replaced the company list (verified 2026-10-05, one ticket; [hubspot.md](hubspot.md#association-writes)). See "Linked Record Format for Mutations" above.
|
|
449
522
|
|
|
450
523
|
### Multi-Select
|
|
451
524
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "softr-vibe-coding",
|
|
3
|
-
"version": "2.13.
|
|
3
|
+
"version": "2.13.1",
|
|
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"
|
|
@@ -34,10 +34,11 @@ Run through this catalog before delivering any block. Every row is a violation o
|
|
|
34
34
|
| Assuming `mutation.enabled === false` always means a code bug | `enabled` is BOTH a parser signal AND a permissions signal. Per the official Softr docs, "`enabled` reflects user permissions." When code looks correct and the Actions tab shows the action listed, the cause is almost always permissions. Test by switching "Preview as" in Studio to an Owner / admin; if it then works, the issue is permissions. Three places to check, in priority order: (1) the block's **Visibility** tab (right panel), (2) **Studio → Users → Data Restrictions → Global data restrictions** — an app-wide layer that easily gets overlooked because it's hidden under Users (not on the block); it overlays every block in the app, and a single restriction on the target table will silently disable every mutation against that table for the affected user group, (3) the data-source PAT scope — if granted read-only, every write fails regardless of UI permissions. See [datasources/writing.md](../datasources/writing.md#how-actions-work-studios-actions-tab) |
|
|
35
35
|
| `deleteRecord.mutate({ id: r.id })` | `deleteRecord.mutate(r.id)` -- just the string |
|
|
36
36
|
| `var { mutateAsync } = useRecordUpdate({...})` -- destructuring the mutate function off the hook | `var updateRecord = useRecordUpdate({...})` -- keep the full object so `.enabled`, `.status`, `.reset()` stay reachable (using `.mutateAsync` itself is fine) |
|
|
37
|
-
| Not calling `refetch()` after mutations | Always `refetch()` in `onSuccess
|
|
37
|
+
| Not calling `refetch()` after mutations | Always `refetch()` in `onSuccess`. The runtime issued no read after a write (measured live 2026-10-05 on a HubSpot-backed block), so without it the list is never re-read. If the source changes other fields itself (HubSpot stamps a closing deal's close date and recalculates its probability a few seconds later), read again a few seconds later too — see [writing.md](../datasources/writing.md#fields-the-source-changes-after-the-write) |
|
|
38
38
|
| Including read-only fields (formula / rollup / aiText / lookup / createdTime / lastModifiedTime / autoNumber) in the `fields` q.select passed to `useRecordCreate` or `useRecordUpdate` | Softr's Action parser silently rejects the **entire** create/update Action — not just the bad alias. Same all-or-nothing failure mode as a renamed/missing column: Studio's Actions tab shows "No actions used in this block yet", `createRecord.enabled` / `updateRecord.enabled` stays `false`, `.mutate()` calls dispatch but resolve to "not yet ready", every OTHER writable field in the same q.select is also lost. Reads handle these field types fine — only the write q.select chokes. Fix: split into separate q.selects per the Three Mappings Pattern (`useRecord` / `useRecords` gets the full select with read-only fields; `useRecordCreate` / `useRecordUpdate` gets a writable-only subset). Diagnostic when the symptom shows up: same bisection procedure as the renamed-column case — strip the write q.select to a known-writable minimum, then add fields back in halves until the Action drops out. Verified 2026-05-22: `blocks/wig-details/wig-details-page.jsx` shared one `wigSelect` between `useRecord` and `useRecordUpdate`; the select included `Wig Tag ID` (formula), `Total client price` (rollup), `Total worker pay` (rollup), `Instrucciones` (aiText) — Update Action stayed disabled until those four read-only fields were lifted out into a separate write-only select. See [datasources/airtable.md](../datasources/airtable.md#maintainability-gotcha) and [datasources/writing.md](../datasources/writing.md). |
|
|
39
|
-
| Linked record as bare string, or `[{ id }]` objects on Softr Database | Array of record-id **strings**: `familyLink: [familyId]` — verified live 2026-08-25 on Softr DB. (The `[{ id }]` object shape was the May 2026 verified form on Airtable-backed blocks; try it if a string-array write fails there.) See [datasources/writing.md](../datasources/writing.md#linked-record-format-for-mutations) |
|
|
40
|
-
|
|
|
39
|
+
| Linked record as bare string, or `[{ id }]` objects on Softr Database | Array of record-id **strings**: `familyLink: [familyId]` — verified live 2026-08-25 on Softr DB. (The `[{ id }]` object shape was the May 2026 verified form on Airtable-backed blocks; try it if a string-array write fails there.) HubSpot associations: the string array worked on create, but an update needs `[{ id }]` (the string array returned 500; verified live 2026-10-05). See [datasources/writing.md](../datasources/writing.md#linked-record-format-for-mutations) |
|
|
40
|
+
| Adding one HubSpot association with `useRecordUpdate`, e.g. `fields: { company: [{ id: newId }] }` | The update **replaced** the whole company list (verified live 2026-10-05, one ticket), and Softr's read afterwards showed no primary company (not checked in HubSpot). Writing the full list back with the new id is untested and would likely drop the primary label too (inferred); or add the link from a workflow that calls HubSpot's associations API — see [hubspot.md](../datasources/hubspot.md#association-writes) |
|
|
41
|
+
| Writing dropdown values as `{ id, label }` objects (the read shape) or hunting for option UUIDs | Write the option **LABEL string**, exactly matching a defined choice — e.g. `status: "Active"` (verified live 2026-08-25; supersedes the April 2026 UUID rule). Keep vocabularies as greppable constants or fetch live via `useFieldOptions` and write `option.label`. **Except on HubSpot**, which takes the choice **id** as a plain string (`"closedwon"`; verified live 2026-10-05, label untested; see [hubspot.md](../datasources/hubspot.md#writing)) |
|
|
41
42
|
| Writing a formatted phone value (`(212) 555-0100`, `212-555-0100`) to a PHONE field | Sanitize to unformatted international format — `+` followed by digits only (`+12125550100`) — before `mutate()`; some datasources (Monday.com, per the official guide) reject formatted values outright. Official sanitizer: `const sanitizePhone = (raw) => raw.replace(/[^\d+]/g, "").replace(/(?!^)\+/g, "");` See [datasources/writing.md](../datasources/writing.md#phone) |
|
|
42
43
|
| Wrapping a `useRecordCreate` payload in `{ fields: {...} }` (copied from an update call) | Create payloads are **FLAT** — `createRecord.mutate({ name: "Jane" })`. Only update payloads nest: `{ recordId, fields: {...} }`. The asymmetry is by design (verified 2026-08-25) |
|
|
43
44
|
| Passing a data hook's options through a variable or wrapper function: `useRecords(buildOpts())` | **Fails to compile** — the options object must be an inline literal at the call site (verified live 2026-08-25; hit in production, fixed by making the wrapper take the hook's *result* instead). Share `q.select` mappings between hooks, never whole options objects |
|
|
@@ -10,9 +10,9 @@ Cross-block communication via `window` globals, the invisible helper block patte
|
|
|
10
10
|
- [Companion Field Helpers](#companion-field-helpers)
|
|
11
11
|
- [Breadcrumb / Back Navigation](#breadcrumb--back-navigation)
|
|
12
12
|
- [Multi-Table Access via Invisible Helper Blocks](#multi-table-access-via-invisible-helper-blocks)
|
|
13
|
-
- [Publisher Template](#publisher-template)
|
|
14
|
-
- [Consumer Pattern](#consumer-pattern)
|
|
15
|
-
- [useWindowData Custom Hook](#usewindowdata-custom-hook)
|
|
13
|
+
- [Publisher Template](#publisher-template-the-invisible-helper-block)
|
|
14
|
+
- [Consumer Pattern](#consumer-pattern-inside-the-main-block)
|
|
15
|
+
- [useWindowData Custom Hook](#usewindowdata-custom-hook-cleaner-consumer)
|
|
16
16
|
- [Triggering Actions in Other Blocks (Bi-Directional Events)](#triggering-actions-in-other-blocks-bi-directional-events)
|
|
17
17
|
- [Advanced Patterns](#advanced-patterns)
|
|
18
18
|
- [Anti-Patterns](#anti-patterns)
|