@agent-native/core 0.119.6 → 0.120.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.
Files changed (91) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +19 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/cli/index.ts +64 -3
  5. package/corpus/core/src/client/MultiTabAssistantChat.tsx +45 -0
  6. package/corpus/core/src/client/use-chat-models.ts +45 -0
  7. package/corpus/core/src/server/analytics.ts +15 -0
  8. package/corpus/core/src/server/auth.ts +17 -13
  9. package/corpus/core/src/server/better-auth-instance.ts +13 -1
  10. package/corpus/core/src/templates/default/package.json +1 -1
  11. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +1 -1
  12. package/corpus/templates/assets/changelog/2026-07-23-new-chat-aligns-with-sidebar-controls.md +6 -0
  13. package/corpus/templates/assets/package.json +1 -1
  14. package/corpus/templates/brain/package.json +1 -1
  15. package/corpus/templates/calendar/package.json +1 -1
  16. package/corpus/templates/clips/package.json +1 -1
  17. package/corpus/templates/content/actions/add-database-item.ts +1 -0
  18. package/corpus/templates/content/actions/set-document-property.ts +0 -4
  19. package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +10 -1
  20. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +10 -4
  21. package/corpus/templates/content/app/components/editor/DocumentInfoPanel.tsx +5 -1
  22. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +6 -0
  23. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +52 -33
  24. package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +16 -4
  25. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +16 -7
  26. package/corpus/templates/content/app/hooks/use-create-page.ts +8 -3
  27. package/corpus/templates/content/app/hooks/use-document-properties.ts +1 -0
  28. package/corpus/templates/content/app/hooks/use-documents.ts +3 -1
  29. package/corpus/templates/content/app/lib/optimistic-document.ts +29 -0
  30. package/corpus/templates/content/changelog/2026-07-21-editing-a-database-property-no-longer-refreshes-unrelated-co.md +6 -0
  31. package/corpus/templates/content/changelog/2026-07-23-new-database-ready.md +6 -0
  32. package/corpus/templates/content/package.json +1 -1
  33. package/corpus/templates/content/shared/api.ts +1 -0
  34. package/corpus/templates/design/package.json +1 -1
  35. package/corpus/templates/dispatch/package.json +1 -1
  36. package/corpus/templates/forms/.agents/skills/form-publishing/SKILL.md +9 -3
  37. package/corpus/templates/forms/AGENTS.md +11 -1
  38. package/corpus/templates/forms/README.md +24 -1
  39. package/corpus/templates/forms/actions/update-form.ts +12 -5
  40. package/corpus/templates/forms/app/components/builder/FieldPropertiesPanel.tsx +199 -1
  41. package/corpus/templates/forms/app/i18n/en-US.ts +13 -1
  42. package/corpus/templates/forms/app/pages/FormBuilderPage.tsx +35 -4
  43. package/corpus/templates/forms/app/pages/FormFillPage.tsx +7 -16
  44. package/corpus/templates/forms/changelog/2026-07-23-forms-can-branch-into-follow-up-questions-and-route-response.md +6 -0
  45. package/corpus/templates/forms/package.json +1 -1
  46. package/corpus/templates/forms/server/handlers/submissions.ts +22 -31
  47. package/corpus/templates/forms/server/lib/integrations.ts +11 -2
  48. package/corpus/templates/forms/server/lib/public-form-ssr.ts +26 -5
  49. package/corpus/templates/forms/server/lib/validate-fields.ts +48 -11
  50. package/corpus/templates/forms/server/plugins/agent-chat.ts +1 -0
  51. package/corpus/templates/forms/shared/conditional.ts +67 -0
  52. package/corpus/templates/macros/package.json +1 -1
  53. package/corpus/templates/mail/package.json +1 -1
  54. package/corpus/templates/slides/package.json +1 -1
  55. package/dist/cli/index.js +53 -2
  56. package/dist/cli/index.js.map +1 -1
  57. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  58. package/dist/client/MultiTabAssistantChat.js +35 -0
  59. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  60. package/dist/client/use-chat-models.d.ts +1 -0
  61. package/dist/client/use-chat-models.d.ts.map +1 -1
  62. package/dist/client/use-chat-models.js +33 -0
  63. package/dist/client/use-chat-models.js.map +1 -1
  64. package/dist/collab/struct-routes.d.ts +1 -1
  65. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  66. package/dist/notifications/routes.d.ts +3 -3
  67. package/dist/provider-api/actions/custom-provider-registration.d.ts +6 -6
  68. package/dist/provider-api/actions/provider-api.d.ts +9 -9
  69. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  70. package/dist/secrets/routes.d.ts +3 -3
  71. package/dist/server/analytics.d.ts +8 -0
  72. package/dist/server/analytics.d.ts.map +1 -1
  73. package/dist/server/analytics.js +16 -0
  74. package/dist/server/analytics.js.map +1 -1
  75. package/dist/server/auth.d.ts.map +1 -1
  76. package/dist/server/auth.js +5 -4
  77. package/dist/server/auth.js.map +1 -1
  78. package/dist/server/better-auth-instance.d.ts +8 -0
  79. package/dist/server/better-auth-instance.d.ts.map +1 -1
  80. package/dist/server/better-auth-instance.js +10 -1
  81. package/dist/server/better-auth-instance.js.map +1 -1
  82. package/dist/server/realtime-token.d.ts +1 -1
  83. package/dist/templates/default/package.json +1 -1
  84. package/package.json +1 -1
  85. package/src/cli/index.ts +64 -3
  86. package/src/client/MultiTabAssistantChat.tsx +45 -0
  87. package/src/client/use-chat-models.ts +45 -0
  88. package/src/server/analytics.ts +15 -0
  89. package/src/server/auth.ts +17 -13
  90. package/src/server/better-auth-instance.ts +13 -1
  91. package/src/templates/default/package.json +1 -1
@@ -197,6 +197,7 @@ import {
197
197
  } from "@/hooks/use-document-properties";
198
198
  import {
199
199
  isDocumentUpdateConflict,
200
+ type DocumentUpdateResult,
200
201
  useDeleteDocument,
201
202
  useDocument,
202
203
  seedDatabaseItemDocumentCaches,
@@ -254,7 +255,10 @@ import { EmojiPicker } from "../EmojiPicker";
254
255
  import {
255
256
  createPreviewDocumentSaveController,
256
257
  deferredPreviewDocumentSave,
258
+ type PreviewDocumentPayload,
257
259
  type PreviewDocumentSaveAdapter,
260
+ type PreviewDocumentSaveDeferred,
261
+ type PreviewDocumentSaveSuccess,
258
262
  } from "../previewDocumentSaveController";
259
263
  import {
260
264
  acquirePreviewDocumentSaveController,
@@ -436,7 +440,8 @@ export function databaseCreatedItemForImmediatePreview(
436
440
  if (returnedItem) return returnedItem;
437
441
  if (!response.createdItemId || !response.createdDocumentId) return null;
438
442
 
439
- const now = args.now ?? new Date().toISOString();
443
+ const now =
444
+ response.createdDocumentUpdatedAt ?? args.now ?? new Date().toISOString();
440
445
  const position = Math.max(
441
446
  0,
442
447
  (response.pagination?.totalItems ?? response.items.length + 1) - 1,
@@ -473,6 +478,42 @@ export function databaseCreatedItemForImmediatePreview(
473
478
  };
474
479
  }
475
480
 
481
+ export function previewDocumentSaveResult(args: {
482
+ result: DocumentUpdateResult;
483
+ payload: PreviewDocumentPayload;
484
+ baseline?: PreviewDocumentPayload;
485
+ contentChanged: boolean;
486
+ }): PreviewDocumentSaveDeferred | PreviewDocumentSaveSuccess {
487
+ const serverDocument = isDocumentUpdateConflict(args.result)
488
+ ? args.result.document
489
+ : args.result;
490
+ const titleSaveObservedExternalBody =
491
+ !args.contentChanged && serverDocument.content !== args.payload.content;
492
+
493
+ if (isDocumentUpdateConflict(args.result) || titleSaveObservedExternalBody) {
494
+ return deferredPreviewDocumentSave("conflict", {
495
+ lastSaved: args.baseline ?? args.payload,
496
+ pending: {
497
+ title: serverDocument.title,
498
+ content: serverDocument.content,
499
+ loadedUpdatedAt: serverDocument.updatedAt,
500
+ loadedContentWasEmpty: isEffectivelyEmptyDocumentContent(
501
+ serverDocument.content,
502
+ ),
503
+ },
504
+ deferredReason: "conflict",
505
+ });
506
+ }
507
+
508
+ return {
509
+ outcome: "saved",
510
+ loadedUpdatedAt: args.result.updatedAt,
511
+ loadedContentWasEmpty: isEffectivelyEmptyDocumentContent(
512
+ args.result.content,
513
+ ),
514
+ };
515
+ }
516
+
476
517
  export function databaseBuilderHydrationSourceForItem(
477
518
  item: ContentDatabaseItem,
478
519
  sources: ContentDatabaseSource[],
@@ -4098,8 +4139,6 @@ function DatabaseItemPreview({
4098
4139
  // ever touch its own row's state. See previewDocumentSaveRegistry.
4099
4140
  const updateDocumentRef = useRef(updateDocument);
4100
4141
  updateDocumentRef.current = updateDocument;
4101
- const queryClientRef = useRef(queryClient);
4102
- queryClientRef.current = queryClient;
4103
4142
  const bodyHydrationPendingRef = useRef(bodyHydrationPending);
4104
4143
  bodyHydrationPendingRef.current = bodyHydrationPending;
4105
4144
  const draftVersionsRef = useRef<Map<string, number | null>>(new Map());
@@ -4244,30 +4283,14 @@ function DatabaseItemPreview({
4244
4283
  },
4245
4284
  {
4246
4285
  onSuccess: (result) => {
4247
- if (isDocumentUpdateConflict(result)) {
4248
- resolve(
4249
- deferredPreviewDocumentSave("conflict", {
4250
- lastSaved: baseline ?? payload,
4251
- pending: {
4252
- title: result.document.title,
4253
- content: result.document.content,
4254
- loadedUpdatedAt: result.document.updatedAt,
4255
- loadedContentWasEmpty: isEffectivelyEmptyDocumentContent(
4256
- result.document.content,
4257
- ),
4258
- },
4259
- deferredReason: "conflict",
4260
- }),
4261
- );
4262
- return;
4263
- }
4264
- resolve({
4265
- outcome: "saved" as const,
4266
- loadedUpdatedAt: result.updatedAt,
4267
- loadedContentWasEmpty: isEffectivelyEmptyDocumentContent(
4268
- result.content,
4269
- ),
4270
- });
4286
+ resolve(
4287
+ previewDocumentSaveResult({
4288
+ result,
4289
+ payload,
4290
+ baseline,
4291
+ contentChanged,
4292
+ }),
4293
+ );
4271
4294
  },
4272
4295
  onError: reject,
4273
4296
  },
@@ -4276,12 +4299,6 @@ function DatabaseItemPreview({
4276
4299
  onSaved: (persistedPayload) => {
4277
4300
  const controller = peekPreviewDocumentSaveController(documentId);
4278
4301
  if (controller) enqueueDraftWrite(controller, "delete", persistedPayload);
4279
- void queryClientRef.current.invalidateQueries({
4280
- queryKey: contentDatabaseQueryKey(databaseDocumentId),
4281
- });
4282
- void queryClientRef.current.invalidateQueries({
4283
- queryKey: ["action", "list-documents"],
4284
- });
4285
4302
  },
4286
4303
  onError: (err) => {
4287
4304
  toast.error(dbText("failedToSavePagePreview"), {
@@ -4876,6 +4893,7 @@ function DatabaseItemPreview({
4876
4893
  {previewDocument.databaseMembership ? (
4877
4894
  <DocumentProperties
4878
4895
  documentId={previewDocument.id}
4896
+ databaseDocumentId={databaseDocumentId}
4879
4897
  canEdit={previewCanEdit}
4880
4898
  popoversPortalled={false}
4881
4899
  />
@@ -4913,6 +4931,7 @@ function DatabaseItemPreview({
4913
4931
  const editor = previewDocument.databaseMembership ? (
4914
4932
  <DocumentBlockFields
4915
4933
  documentId={previewDocument.id}
4934
+ databaseDocumentId={databaseDocumentId}
4916
4935
  canEdit={previewCanEdit}
4917
4936
  primaryEditor={primaryEditor}
4918
4937
  />
@@ -130,8 +130,8 @@ export interface PreviewDocumentSaveController {
130
130
  /** Adopt `payload` as the confirmed-saved baseline (no save scheduled). */
131
131
  mark(payload: PreviewDocumentPayload): void;
132
132
  /**
133
- * Adopt a fresher server baseline while retaining the user's pending title
134
- * and content. Used only after an explicit "keep local draft" choice.
133
+ * Adopt a fresher server baseline while retaining only fields the user
134
+ * changed locally. Used only after an explicit "keep local draft" choice.
135
135
  */
136
136
  rebasePending(payload: PreviewDocumentPayload): void;
137
137
  /** Replace callbacks captured by an older preview mount. */
@@ -261,8 +261,7 @@ export function createPreviewDocumentSaveController(
261
261
  // the controller was created/last marked — e.g. "empty" for a
262
262
  // brand-new page — forever, even after real content has been saved.
263
263
  const success = asSaveSuccess(result);
264
- lastSaved = {
265
- ...attempted,
264
+ const savedMetadata = {
266
265
  ...(success?.loadedUpdatedAt !== undefined
267
266
  ? { loadedUpdatedAt: success.loadedUpdatedAt }
268
267
  : {}),
@@ -270,6 +269,15 @@ export function createPreviewDocumentSaveController(
270
269
  ? { loadedContentWasEmpty: success.loadedContentWasEmpty }
271
270
  : {}),
272
271
  };
272
+ lastSaved = {
273
+ ...attempted,
274
+ ...savedMetadata,
275
+ };
276
+ // A later keystroke starts from `pending`, including while this save is
277
+ // in flight. Rebase that trailing payload onto our own successful write
278
+ // so its next CAS does not mistake the preceding save for an external
279
+ // change.
280
+ pending = { ...pending, ...savedMetadata };
273
281
  hasSavedLocally = true;
274
282
  deferredReason = null;
275
283
  inFlight = null;
@@ -336,9 +344,13 @@ export function createPreviewDocumentSaveController(
336
344
  },
337
345
  rebasePending(payload: PreviewDocumentPayload) {
338
346
  clearTimer();
347
+ const titleChangedLocally = pending.title !== lastSaved.title;
348
+ const contentChangedLocally = pending.content !== lastSaved.content;
339
349
  lastSaved = { ...payload };
340
350
  pending = {
341
351
  ...pending,
352
+ title: titleChangedLocally ? pending.title : payload.title,
353
+ content: contentChangedLocally ? pending.content : payload.content,
342
354
  loadedUpdatedAt: payload.loadedUpdatedAt,
343
355
  loadedContentWasEmpty: payload.loadedContentWasEmpty,
344
356
  };
@@ -121,6 +121,10 @@ import {
121
121
  filterDocumentTreeDocuments,
122
122
  } from "@/hooks/use-documents";
123
123
  import { useLocalStorage } from "@/hooks/use-local-storage";
124
+ import {
125
+ markDocumentCreationPending,
126
+ shouldCreateDocumentOptimistically,
127
+ } from "@/lib/optimistic-document";
124
128
  import { cn } from "@/lib/utils";
125
129
 
126
130
  import {
@@ -747,7 +751,12 @@ export function DocumentSidebar({
747
751
  optimisticId?: string,
748
752
  rootFilesDatabaseId?: string,
749
753
  ) => {
750
- if (localFileMode) {
754
+ if (
755
+ !shouldCreateDocumentOptimistically({
756
+ localFileMode,
757
+ filesDatabaseId: rootFilesDatabaseId,
758
+ })
759
+ ) {
751
760
  try {
752
761
  const created = await createDocument.mutateAsync({
753
762
  title: "",
@@ -774,7 +783,7 @@ export function DocumentSidebar({
774
783
 
775
784
  const id = optimisticId ?? nanoid();
776
785
  const now = new Date().toISOString();
777
- const tempDoc: Document = {
786
+ const tempDoc = markDocumentCreationPending({
778
787
  id,
779
788
  parentId: parentId ?? null,
780
789
  title: "",
@@ -789,7 +798,7 @@ export function DocumentSidebar({
789
798
  canManage: true,
790
799
  createdAt: now,
791
800
  updatedAt: now,
792
- };
801
+ });
793
802
 
794
803
  // Optimistically inject into caches so UI updates immediately
795
804
  queryClient.setQueryData(LIST_DOCUMENTS_QUERY_KEY, (old: any) => {
@@ -824,14 +833,14 @@ export function DocumentSidebar({
824
833
  spaceId: parentId ? undefined : rootSpaceId,
825
834
  });
826
835
  const nextId = created?.id || id;
836
+ queryClient.setQueryData(
837
+ ["action", "get-document", { id: nextId }],
838
+ created,
839
+ );
827
840
  if (nextId !== id) {
828
841
  queryClient.removeQueries({
829
842
  queryKey: ["action", "get-document", { id }],
830
843
  });
831
- queryClient.setQueryData(
832
- ["action", "get-document", { id: nextId }],
833
- created,
834
- );
835
844
  navigateToDocument(nextId);
836
845
  }
837
846
  // Replace optimistic doc with real server doc + clear any 404 error
@@ -12,6 +12,7 @@ import {
12
12
  import { useContentSpaces } from "@/hooks/use-content-spaces";
13
13
  import { useCreateDocument } from "@/hooks/use-documents";
14
14
  import { useLocalStorage } from "@/hooks/use-local-storage";
15
+ import { markDocumentCreationPending } from "@/lib/optimistic-document";
15
16
 
16
17
  const LIST_DOCUMENTS_QUERY_KEY = [
17
18
  "action",
@@ -63,7 +64,7 @@ export function useCreatePage(opts?: {
63
64
  }
64
65
  const id = nanoid();
65
66
  const now = new Date().toISOString();
66
- const tempDoc: Document = {
67
+ const tempDoc = markDocumentCreationPending({
67
68
  id,
68
69
  parentId: parentId ?? null,
69
70
  title: "",
@@ -75,7 +76,7 @@ export function useCreatePage(opts?: {
75
76
  visibility: "private",
76
77
  createdAt: now,
77
78
  updatedAt: now,
78
- };
79
+ });
79
80
 
80
81
  queryClient.setQueryData(LIST_DOCUMENTS_QUERY_KEY, (old: any) => {
81
82
  const docs: Document[] =
@@ -90,12 +91,16 @@ export function useCreatePage(opts?: {
90
91
  }
91
92
 
92
93
  const persist = async () => {
93
- await createDocument.mutateAsync({
94
+ const created = await createDocument.mutateAsync({
94
95
  id,
95
96
  title: "",
96
97
  parentId: parentId ?? undefined,
97
98
  spaceId,
98
99
  });
100
+ queryClient.setQueryData(
101
+ ["action", "get-document", { id: created.id }],
102
+ created,
103
+ );
99
104
  // Replace optimistic doc with real server doc + clear any 404 error
100
105
  // state from the in-flight fetch that ran before create completed.
101
106
  queryClient.invalidateQueries({
@@ -70,6 +70,7 @@ export function useSetDocumentProperty(
70
70
  DocumentPropertiesResponse,
71
71
  SetDocumentPropertyRequest
72
72
  >("set-document-property", {
73
+ skipActionQueryInvalidation: true,
73
74
  onMutate: async (variables) => {
74
75
  await queryClient.cancelQueries(
75
76
  contentDatabaseQueryFilter(databaseDocumentId),
@@ -342,7 +342,9 @@ export function useUpdatePreviewDocumentDraft() {
342
342
  expectedTitle: string;
343
343
  expectedContent: string;
344
344
  }
345
- >("update-preview-document-draft");
345
+ >("update-preview-document-draft", {
346
+ skipActionQueryInvalidation: true,
347
+ });
346
348
  }
347
349
 
348
350
  export function useCreateDocument() {
@@ -0,0 +1,29 @@
1
+ import type { Document } from "@shared/api";
2
+
3
+ const pendingDocumentCreation = Symbol("pendingDocumentCreation");
4
+
5
+ type PendingDocument = Document & {
6
+ [pendingDocumentCreation]?: true;
7
+ };
8
+
9
+ export function markDocumentCreationPending(document: Document): Document {
10
+ return Object.assign(document, { [pendingDocumentCreation]: true as const });
11
+ }
12
+
13
+ export function isDocumentCreationPending(document: Document): boolean {
14
+ return (document as PendingDocument)[pendingDocumentCreation] === true;
15
+ }
16
+
17
+ export function isDatabaseChoicePending(
18
+ document: Document,
19
+ databaseCreationPending: boolean,
20
+ ): boolean {
21
+ return databaseCreationPending || isDocumentCreationPending(document);
22
+ }
23
+
24
+ export function shouldCreateDocumentOptimistically(args: {
25
+ localFileMode: boolean;
26
+ filesDatabaseId?: string;
27
+ }): boolean {
28
+ return !args.localFileMode || Boolean(args.filesDatabaseId);
29
+ }
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-21
4
+ ---
5
+
6
+ Editing a database page no longer refreshes unrelated Content workspace data or interrupts typing in the originating tab.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-23
4
+ ---
5
+
6
+ New pages wait until they are ready before offering database conversion.
@@ -4,7 +4,7 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
7
- "dev": "agent-native dev",
7
+ "dev": "agent-native dev --inspect",
8
8
  "dev:database": "node scripts/check-native-deps.mjs && node scripts/dev-database.mjs",
9
9
  "build": "agent-native build",
10
10
  "start": "agent-native start",
@@ -749,6 +749,7 @@ export interface ContentDatabaseResponse {
749
749
  };
750
750
  createdItemId?: string;
751
751
  createdDocumentId?: string;
752
+ createdDocumentUpdatedAt?: string;
752
753
  duplicatedItemId?: string;
753
754
  duplicatedDocumentId?: string;
754
755
  duplicatedItemIds?: string[];
@@ -4,7 +4,7 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
7
- "dev": "agent-native dev",
7
+ "dev": "agent-native dev --inspect",
8
8
  "build": "agent-native build",
9
9
  "start": "agent-native start",
10
10
  "test": "vitest --run",
@@ -4,7 +4,7 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
7
- "dev": "agent-native dev",
7
+ "dev": "agent-native dev --inspect",
8
8
  "build": "agent-native build",
9
9
  "start": "agent-native start",
10
10
  "typecheck": "agent-native typecheck",
@@ -77,7 +77,7 @@ Each form has a `settings` JSON object:
77
77
  | `showProgressBar` | boolean | Show progress bar for multi-section forms |
78
78
  | `emailOnNewResponses` | boolean | Email the form owner's account when someone submits a response |
79
79
  | `anonymous` | boolean | Suppress IP, submitter identity, chat/run ids, page URL, and client-surface metadata for every response |
80
- | `integrations` | array | Webhook/Slack/Discord notification configs |
80
+ | `integrations` | array | Webhook/Slack/Discord/Google Sheets notification configs |
81
81
 
82
82
  For a genuinely anonymous form, set `anonymous: true` when creating the form.
83
83
  Do not describe an ordinary published form as anonymous: published forms accept
@@ -91,9 +91,15 @@ Forms can notify external services on submission:
91
91
  | Type | Description |
92
92
  | --------------- | ------------------------------- |
93
93
  | `webhook` | POST JSON to any URL |
94
- | `slack` | Send to a Slack channel |
94
+ | `slack` | Send to a Slack channel through an Incoming Webhook URL |
95
95
  | `discord` | Send to a Discord webhook |
96
- | `google-sheets` | Append row to a Google Sheet |
96
+ | `google-sheets` | Send response JSON to a deployed Apps Script `/exec` URL |
97
+
98
+ These are outbound form destinations configured in the form builder's
99
+ **Integrations** tab. They are separate from the managed Slack/Messaging
100
+ connection. Google Sheets Apps Script handlers should parse
101
+ `JSON.parse(e.postData.contents)`; a spreadsheet URL or `/dev` URL will not
102
+ receive submissions.
97
103
 
98
104
  ## Related Skills
99
105
 
@@ -40,7 +40,17 @@ ladder.
40
40
  - To email the form owner when someone submits a response, set
41
41
  `settings.emailOnNewResponses: true` through `create-form` or `update-form`.
42
42
  Delivery uses the configured framework email provider (`RESEND_API_KEY` or
43
- `SENDGRID_API_KEY`) and sends to the form owner's account email.
43
+ `SENDGRID_API_KEY`) in the form owner's request context and sends to the form
44
+ owner's account email. A public submission can succeed even when delivery
45
+ fails, so check the server logs and provider configuration when debugging.
46
+ - Conditional fields use `conditional: { fieldId, operator, value }`. The
47
+ `fieldId` must reference an earlier field; supported operators are `equals`,
48
+ `not_equals`, and `contains`. Hidden fields and their stale values are not
49
+ persisted or delivered to integrations.
50
+ - Form integrations are outbound webhooks. Slack requires an Incoming Webhook
51
+ URL, and Google Sheets requires a deployed Google Apps Script `/exec` URL that
52
+ parses `JSON.parse(e.postData.contents)` and appends the received values.
53
+ They are separate from the managed Slack/Messaging connection.
44
54
  - Form UX should stay focused: clear labels, sensible validation, minimal
45
55
  required fields, and progressive disclosure for advanced settings.
46
56
  - Public form submission endpoints must be intentionally public; keep management
@@ -17,7 +17,8 @@ them.
17
17
  - Shipped field types: text, email, number, long text, select, multi-select,
18
18
  checkbox, radio, date, rating, and scale.
19
19
  - Submissions stored in SQL with a per-response detail view and dashboard.
20
- - Route submissions to webhooks, Slack, Discord, or Google Sheets.
20
+ - Route submissions to webhooks, Slack, Discord, or Google Sheets via Apps
21
+ Script.
21
22
  - Publish public form URLs with a thank-you message.
22
23
 
23
24
  ## Develop locally
@@ -32,3 +33,25 @@ pnpm dev
32
33
  ```
33
34
 
34
35
  Full docs: [agent-native.com/docs/template-forms](https://agent-native.com/docs/template-forms).
36
+
37
+ ## Submission destinations
38
+
39
+ Open a form's **Integrations** tab and save a destination before publishing.
40
+ Slack uses an Incoming Webhook URL. Google Sheets uses a deployed Apps Script
41
+ web app URL ending in `/exec` (not a spreadsheet URL or `/dev` URL). The script
42
+ should parse the JSON body and append the values, for example:
43
+
44
+ ```ts
45
+ function doPost(e) {
46
+ const payload = JSON.parse(e.postData.contents);
47
+ SpreadsheetApp.getActiveSpreadsheet()
48
+ .getSheets()[0]
49
+ .appendRow([payload.submittedAt, payload.formTitle, payload.responseId]);
50
+ return ContentService.createTextOutput(JSON.stringify({ ok: true }));
51
+ }
52
+ ```
53
+
54
+ Deploy it as a web app that executes as you and allows anyone with the URL to
55
+ access it, then paste the deployed `/exec` URL into Forms. The destination
56
+ receives the form metadata plus one property per field label; duplicate labels
57
+ are disambiguated with the field ID.
@@ -85,22 +85,29 @@ export default defineAction({
85
85
  updates.fields = JSON.stringify(parsedFields);
86
86
  }
87
87
  if (args.settings !== undefined) {
88
- let parsedSettings: FormSettings;
88
+ let incomingSettings: FormSettings;
89
89
  if (typeof args.settings === "string") {
90
90
  try {
91
- parsedSettings = JSON.parse(args.settings) as FormSettings;
92
- updates.settings = args.settings;
91
+ incomingSettings = JSON.parse(args.settings) as FormSettings;
93
92
  } catch {
94
93
  throw new Error("--settings must be valid JSON");
95
94
  }
96
95
  } else {
97
- parsedSettings = args.settings as unknown as FormSettings;
98
- updates.settings = JSON.stringify(args.settings);
96
+ incomingSettings = args.settings as unknown as FormSettings;
99
97
  }
98
+ let existingSettings: FormSettings = {};
99
+ try {
100
+ existingSettings = JSON.parse(existing.settings) as FormSettings;
101
+ } catch {
102
+ // Keep malformed legacy settings recoverable by replacing them with
103
+ // the valid settings supplied by this update.
104
+ }
105
+ const parsedSettings = { ...existingSettings, ...incomingSettings };
100
106
  // Reject blocked integration URLs at save time (private IPs,
101
107
  // cloud-metadata, non-http(s) schemes). fireIntegrations also
102
108
  // re-checks at runtime as defense-in-depth.
103
109
  assertIntegrationUrlsAllowed(parsedSettings);
110
+ updates.settings = JSON.stringify(parsedSettings);
104
111
  }
105
112
  if (args.status !== undefined) updates.status = args.status;
106
113