@notionhq/apps 0.0.16 → 0.0.18

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 (77) hide show
  1. package/AGENTS.md +27 -0
  2. package/README.md +86 -36
  3. package/dist/connections.d.ts +39 -11
  4. package/dist/connections.d.ts.map +1 -1
  5. package/dist/connections.js +85 -21
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +2 -0
  9. package/dist/notion-as-code/database.d.ts +89 -48
  10. package/dist/notion-as-code/database.d.ts.map +1 -1
  11. package/dist/notion-as-code/database.js +132 -45
  12. package/dist/notion-as-code/database.test.d.ts +2 -0
  13. package/dist/notion-as-code/database.test.d.ts.map +1 -0
  14. package/dist/notion-as-code/handles.d.ts +2 -0
  15. package/dist/notion-as-code/handles.d.ts.map +1 -1
  16. package/dist/notion-as-code/handles.js +2 -0
  17. package/dist/notion-as-code/index.d.ts +5 -2
  18. package/dist/notion-as-code/index.d.ts.map +1 -1
  19. package/dist/notion-as-code/intents.d.ts +36 -3
  20. package/dist/notion-as-code/intents.d.ts.map +1 -1
  21. package/dist/notion-as-code/schema.d.ts +8 -4
  22. package/dist/notion-as-code/schema.d.ts.map +1 -1
  23. package/dist/notion-as-code/view.d.ts +12 -0
  24. package/dist/notion-as-code/view.d.ts.map +1 -0
  25. package/dist/notion-as-code/view.js +13 -0
  26. package/dist/notion-as-code/views-types.test.d.ts +2 -0
  27. package/dist/notion-as-code/views-types.test.d.ts.map +1 -0
  28. package/dist/notion-as-code/views.d.ts +488 -0
  29. package/dist/notion-as-code/views.d.ts.map +1 -0
  30. package/dist/notion-as-code/views.js +0 -0
  31. package/dist/oauth.d.ts +11 -0
  32. package/dist/oauth.d.ts.map +1 -0
  33. package/dist/oauth.js +26 -0
  34. package/dist/providers.generated.d.ts +26 -86
  35. package/dist/providers.generated.d.ts.map +1 -1
  36. package/dist/providers.generated.js +54 -66
  37. package/dist/sync.d.ts +33 -15
  38. package/dist/sync.d.ts.map +1 -1
  39. package/dist/sync.js +12 -0
  40. package/dist/triggers.generated.d.ts +3 -3
  41. package/dist/triggers.generated.d.ts.map +1 -1
  42. package/dist/workflow-state.d.ts +13 -0
  43. package/dist/workflow-state.d.ts.map +1 -0
  44. package/dist/workflow-state.js +107 -0
  45. package/dist/workflow.d.ts +56 -11
  46. package/dist/workflow.d.ts.map +1 -1
  47. package/dist/workflow.js +123 -10
  48. package/docs/BUILD.md +96 -0
  49. package/docs/CONNECTIONS.md +70 -22
  50. package/package.json +1 -1
  51. package/skills/connections/SKILL.md +7 -8
  52. package/skills/notion-as-code/SKILL.md +88 -50
  53. package/skills/sync/SKILL.md +21 -18
  54. package/skills/workflow/SKILL.md +52 -3
  55. package/src/cli/build.test.ts +124 -64
  56. package/src/connections.test.ts +205 -41
  57. package/src/connections.ts +144 -38
  58. package/src/index.ts +2 -0
  59. package/src/notion-as-code/database.test.ts +661 -0
  60. package/src/notion-as-code/database.ts +346 -129
  61. package/src/notion-as-code/handles.ts +2 -0
  62. package/src/notion-as-code/index.ts +71 -1
  63. package/src/notion-as-code/intents.ts +41 -4
  64. package/src/notion-as-code/schema.ts +11 -3
  65. package/src/notion-as-code/view.ts +23 -0
  66. package/src/notion-as-code/views-types.test.ts +59 -0
  67. package/src/notion-as-code/views.ts +573 -0
  68. package/src/oauth.ts +40 -0
  69. package/src/providers.generated.ts +68 -163
  70. package/src/sync.test.ts +295 -0
  71. package/src/sync.ts +85 -21
  72. package/src/triggers.generated.ts +4 -4
  73. package/src/workflow-connections-types.test.ts +16 -10
  74. package/src/workflow-state.ts +152 -0
  75. package/src/workflow-types.test.ts +88 -16
  76. package/src/workflow.test.ts +374 -18
  77. package/src/workflow.ts +211 -23
@@ -8,7 +8,13 @@ import { afterEach, beforeEach, describe, expect, expectTypeOf, it, vi, type Moc
8
8
  import { ExecutionError } from "./error.js";
9
9
  import type { RunMetadata } from "./runtime-metadata.js";
10
10
  import { triggers, type NotionPageCreatedEvent } from "./triggers.generated.js";
11
- import { connections, workflow, type StepContext, type WorkflowContext } from "./workflow.js";
11
+ import {
12
+ connections,
13
+ workflow,
14
+ type StepContext,
15
+ type WorkflowContext,
16
+ type WorkflowWaitDuration,
17
+ } from "./workflow.js";
12
18
 
13
19
  let stdoutSpy: Mock<typeof process.stdout.write>;
14
20
  let checkpointDirectory: string | undefined;
@@ -17,6 +23,7 @@ let metadataDirectory: string | undefined;
17
23
  beforeEach(async () => {
18
24
  stdoutSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
19
25
  delete process.env.NOTION_WORKFLOW_STEP_DIRECTORY;
26
+ process.env.NOTION_WORKFLOW_CAPABILITY_KEY = "workflow-key";
20
27
  checkpointDirectory = undefined;
21
28
  metadataDirectory = await mkdtemp(join(tmpdir(), "notion-workflow-metadata-"));
22
29
  await writeRunMetadata();
@@ -25,8 +32,12 @@ beforeEach(async () => {
25
32
  afterEach(async () => {
26
33
  stdoutSpy.mockRestore();
27
34
  delete process.env.NOTION_WORKFLOW_STEP_DIRECTORY;
35
+ delete process.env.NOTION_WORKFLOW_CAPABILITY_KEY;
28
36
  delete process.env.NOTION_INPUT_PAYLOAD_PATH;
29
37
  delete process.env.NOTION_RUN_METADATA_PATH;
38
+ delete process.env.NOTION_API_BASE_URL;
39
+ delete process.env.NOTION_API_TOKEN;
40
+ vi.unstubAllGlobals();
30
41
  if (checkpointDirectory) {
31
42
  await rm(checkpointDirectory, { recursive: true, force: true });
32
43
  }
@@ -37,8 +48,8 @@ afterEach(async () => {
37
48
 
38
49
  const event = { type: "notion.page.created" } as NotionPageCreatedEvent;
39
50
 
40
- async function createWorkflowContext(): Promise<WorkflowContext<readonly []>> {
41
- let context: WorkflowContext<readonly []> | undefined;
51
+ async function createWorkflowContext(): Promise<WorkflowContext<Record<never, never>>> {
52
+ let context: WorkflowContext<Record<never, never>> | undefined;
42
53
  const definition = workflow({
43
54
  name: "Test workflow",
44
55
  description: "Captures its workflow context",
@@ -80,6 +91,167 @@ function emittedStepEvents(): Array<{
80
91
  }
81
92
 
82
93
  describe("step", () => {
94
+ it("exposes workflow state only while a step callback is running", async () => {
95
+ const context = await createWorkflowContext();
96
+ let escapedState: StepContext["state"] | undefined;
97
+
98
+ await context.step("Capture state", ({ state }) => {
99
+ escapedState = state;
100
+ });
101
+
102
+ await expect(escapedState?.get("cursor")).rejects.toThrow(
103
+ "state.get() must be called inside context.step().",
104
+ );
105
+ });
106
+
107
+ it("reads and writes durable workflow state inside a step", async () => {
108
+ process.env.NOTION_API_BASE_URL = "https://api.example.com";
109
+ process.env.NOTION_API_TOKEN = "workflow-token";
110
+ const fetchMock = vi
111
+ .fn<typeof fetch>()
112
+ .mockResolvedValueOnce(
113
+ new Response(JSON.stringify({ exists: true, value: "cursor-1" }), {
114
+ status: 200,
115
+ headers: { "content-type": "application/json" },
116
+ }),
117
+ )
118
+ .mockResolvedValueOnce(
119
+ new Response(JSON.stringify({}), {
120
+ status: 200,
121
+ headers: { "content-type": "application/json" },
122
+ }),
123
+ );
124
+ vi.stubGlobal("fetch", fetchMock);
125
+ const context = await createWorkflowContext();
126
+
127
+ const cursor = await context.step("Update cursor", async ({ state }) => {
128
+ const current = await state.get("cursor");
129
+ await state.set("cursor", "cursor-2");
130
+ return current;
131
+ });
132
+
133
+ expect(cursor).toBe("cursor-1");
134
+ expect(fetchMock).toHaveBeenNthCalledWith(
135
+ 1,
136
+ "https://api.example.com/api/v3/workersWorkflowState",
137
+ expect.objectContaining({
138
+ method: "POST",
139
+ headers: expect.objectContaining({
140
+ authorization: "Bearer workflow-token",
141
+ }),
142
+ body: JSON.stringify({
143
+ operation: "get",
144
+ key: "cursor",
145
+ workerId: "worker-id",
146
+ capabilityKey: "workflow-key",
147
+ }),
148
+ }),
149
+ );
150
+ expect(fetchMock).toHaveBeenNthCalledWith(
151
+ 2,
152
+ "https://api.example.com/api/v3/workersWorkflowState",
153
+ expect.objectContaining({
154
+ method: "POST",
155
+ body: expect.stringMatching(
156
+ /^\{"operation":"commit","operationId":"run-group-id:[^"]+","mutations":\[\{"operation":"set","key":"cursor","value":"cursor-2"\}\],"workerId":"worker-id","capabilityKey":"workflow-key"\}$/,
157
+ ),
158
+ }),
159
+ );
160
+ });
161
+
162
+ it("discards buffered state when the step callback throws", async () => {
163
+ const fetchMock = vi.fn<typeof fetch>();
164
+ vi.stubGlobal("fetch", fetchMock);
165
+ const context = await createWorkflowContext();
166
+
167
+ await expect(
168
+ context.step("Do something", ({ state }) => {
169
+ state.set("foo", 42);
170
+ throw new Error("crash");
171
+ }),
172
+ ).rejects.toThrow("crash");
173
+ expect(fetchMock).not.toHaveBeenCalled();
174
+ });
175
+
176
+ it("reads buffered state before the step commits", async () => {
177
+ process.env.NOTION_API_BASE_URL = "https://api.example.com";
178
+ process.env.NOTION_API_TOKEN = "workflow-token";
179
+ const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(
180
+ new Response(JSON.stringify({ success: true }), {
181
+ status: 200,
182
+ headers: { "content-type": "application/json" },
183
+ }),
184
+ );
185
+ vi.stubGlobal("fetch", fetchMock);
186
+ const context = await createWorkflowContext();
187
+
188
+ const value = await context.step("Write then read", async ({ state }) => {
189
+ state.set("foo", 42);
190
+ return state.get("foo");
191
+ });
192
+
193
+ expect(value).toBe(42);
194
+ expect(fetchMock).toHaveBeenCalledOnce();
195
+ });
196
+
197
+ it("coalesces repeated mutations of a key within the step transaction", async () => {
198
+ process.env.NOTION_API_BASE_URL = "https://api.example.com";
199
+ process.env.NOTION_API_TOKEN = "workflow-token";
200
+ const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(
201
+ new Response(JSON.stringify({ success: true }), {
202
+ status: 200,
203
+ headers: { "content-type": "application/json" },
204
+ }),
205
+ );
206
+ vi.stubGlobal("fetch", fetchMock);
207
+ const context = await createWorkflowContext();
208
+
209
+ const value = await context.step("Mutate repeatedly", async ({ state }) => {
210
+ state.set("cursor", "first");
211
+ state.set("cursor", undefined);
212
+ state.set("cursor", "final");
213
+ return state.get("cursor");
214
+ });
215
+
216
+ expect(value).toBe("final");
217
+ expect(fetchMock).toHaveBeenCalledOnce();
218
+ expect(fetchMock).toHaveBeenCalledWith(
219
+ "https://api.example.com/api/v3/workersWorkflowState",
220
+ expect.objectContaining({
221
+ body: expect.stringContaining(
222
+ '"mutations":[{"operation":"set","key":"cursor","value":"final"}]',
223
+ ),
224
+ }),
225
+ );
226
+ });
227
+
228
+ it("deletes a key by setting it to undefined", async () => {
229
+ process.env.NOTION_API_BASE_URL = "https://api.example.com";
230
+ process.env.NOTION_API_TOKEN = "workflow-token";
231
+ const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(
232
+ new Response(JSON.stringify({ success: true }), {
233
+ status: 200,
234
+ headers: { "content-type": "application/json" },
235
+ }),
236
+ );
237
+ vi.stubGlobal("fetch", fetchMock);
238
+ const context = await createWorkflowContext();
239
+
240
+ const isDeleted = await context.step("Delete state", async ({ state }) => {
241
+ state.set("foo", 42);
242
+ state.set("foo", undefined);
243
+ return (await state.get("foo")) === undefined;
244
+ });
245
+
246
+ expect(isDeleted).toBe(true);
247
+ expect(fetchMock).toHaveBeenCalledWith(
248
+ "https://api.example.com/api/v3/workersWorkflowState",
249
+ expect.objectContaining({
250
+ body: expect.stringContaining('"mutations":[{"operation":"delete","key":"foo"}]'),
251
+ }),
252
+ );
253
+ });
254
+
83
255
  it("provides required run metadata in the workflow context", async () => {
84
256
  const context = await createWorkflowContext();
85
257
 
@@ -683,6 +855,162 @@ describe("step", () => {
683
855
  });
684
856
  });
685
857
 
858
+ describe("wait.until", () => {
859
+ it("stops the workflow and returns its absolute resume time", async () => {
860
+ const now = Date.parse("2026-09-17T09:00:00.000Z");
861
+ const resumeAt = new Date(now + 60_000);
862
+ const dateNowSpy = vi.spyOn(Date, "now").mockReturnValue(now);
863
+ let continued = false;
864
+ const definition = workflow({
865
+ name: "Wait for launch",
866
+ description: "Waits until launch time",
867
+ triggers: [triggers.notionPageCreated()],
868
+ handler: async (_event, context) => {
869
+ await context.wait.until("Launch time", { at: resumeAt });
870
+ continued = true;
871
+ },
872
+ });
873
+
874
+ try {
875
+ await expect(definition.handler(event, { concreteOutput: true })).resolves.toEqual({
876
+ status: "waiting",
877
+ wait: {
878
+ type: "until",
879
+ name: "Launch time",
880
+ resumeAtMs: resumeAt.getTime(),
881
+ },
882
+ });
883
+ expect(continued).toBe(false);
884
+ } finally {
885
+ dateNowSpy.mockRestore();
886
+ }
887
+ });
888
+
889
+ it("anchors a relative wait in its durable step checkpoint", async () => {
890
+ const directory = await createCheckpointDirectory();
891
+ const initialNow = Date.parse("2026-09-17T09:00:00.000Z");
892
+ const dateNowSpy = vi.spyOn(Date, "now").mockReturnValue(initialNow);
893
+ let continued = false;
894
+ const definition = workflow({
895
+ name: "Wait before following up",
896
+ description: "Waits before continuing",
897
+ triggers: [triggers.notionPageCreated()],
898
+ handler: async (_event, context) => {
899
+ await context.wait.until("Follow-up delay", {
900
+ after: { minutes: 30, seconds: 15 },
901
+ });
902
+ continued = true;
903
+ },
904
+ });
905
+
906
+ try {
907
+ const firstResult = await definition.handler(event, { concreteOutput: true });
908
+ expect(firstResult).toMatchObject({
909
+ status: "waiting",
910
+ wait: { resumeAtMs: initialNow + 1_815_000 },
911
+ });
912
+
913
+ const successEvent = emittedStepEvents().find(({ event }) => event === "success");
914
+ const stepKey = successEvent?.value.key;
915
+ const resumeAtMs = successEvent?.value.value;
916
+ if (typeof stepKey !== "string" || typeof resumeAtMs !== "number") {
917
+ throw new Error("Wait checkpoint event was not emitted");
918
+ }
919
+ await writeFile(
920
+ checkpointPath(directory, stepKey),
921
+ JSON.stringify({
922
+ version: 0,
923
+ key: stepKey,
924
+ value: resumeAtMs,
925
+ sourceRunId: "first-run",
926
+ sourceStepId: "wait-step",
927
+ }),
928
+ );
929
+
930
+ dateNowSpy.mockReturnValue(initialNow + 7_200_000);
931
+ await expect(definition.handler(event, { concreteOutput: true })).resolves.toEqual({
932
+ status: "success",
933
+ });
934
+ expect(continued).toBe(true);
935
+ } finally {
936
+ dateNowSpy.mockRestore();
937
+ }
938
+ });
939
+
940
+ it("uses composite keys to distinguish waits in a loop", async () => {
941
+ const now = Date.parse("2026-09-17T09:00:00.000Z");
942
+ const dateNowSpy = vi.spyOn(Date, "now").mockReturnValue(now);
943
+ const itemIds = ["first", "second"];
944
+ const definition = workflow({
945
+ name: "Wait between items",
946
+ description: "Uses one stable display name for repeated waits",
947
+ triggers: [triggers.notionPageCreated()],
948
+ handler: async (_event, context) => {
949
+ for (const itemId of itemIds) {
950
+ await context.wait.until("Item delay", {
951
+ key: ["item-delay", itemId],
952
+ at: new Date(now),
953
+ });
954
+ }
955
+ },
956
+ });
957
+
958
+ try {
959
+ await expect(definition.handler(event, { concreteOutput: true })).resolves.toEqual({
960
+ status: "success",
961
+ });
962
+ expect(
963
+ emittedStepEvents()
964
+ .filter(({ event }) => event === "success")
965
+ .map(({ value }) => value.key),
966
+ ).toEqual([
967
+ workflowStepKey(["wait", "until", "item-delay", "first"]),
968
+ workflowStepKey(["wait", "until", "item-delay", "second"]),
969
+ ]);
970
+ } finally {
971
+ dateNowSpy.mockRestore();
972
+ }
973
+ });
974
+
975
+ it("rejects waits that can outlive their durable checkpoint", async () => {
976
+ const definition = workflow({
977
+ name: "Wait too long",
978
+ description: "Rejects an unsupported wait",
979
+ triggers: [triggers.notionPageCreated()],
980
+ handler: async (_event, context) => {
981
+ await context.wait.until("Beyond retention", {
982
+ after: { days: 7 },
983
+ });
984
+ },
985
+ });
986
+
987
+ await expect(definition.handler(event, { concreteOutput: true })).rejects.toThrow(
988
+ "Workflow waits must be shorter than 7 days.",
989
+ );
990
+ });
991
+
992
+ it.each([
993
+ [{ hours: 0 }, "positive finite integers"],
994
+ [{ minutes: 1.5 }, "positive finite integers"],
995
+ [{}, "at least one unit"],
996
+ ])("rejects invalid relative durations", async (after, expectedMessage) => {
997
+ const definition = workflow({
998
+ name: "Wait for a valid duration",
999
+ description: "Rejects an invalid duration",
1000
+ triggers: [triggers.notionPageCreated()],
1001
+ handler: async (_event, context) => {
1002
+ await context.wait.until("Invalid duration", {
1003
+ after: after as WorkflowWaitDuration,
1004
+ });
1005
+ },
1006
+ });
1007
+
1008
+ await expect(definition.handler(event, { concreteOutput: true })).rejects.toThrow(
1009
+ expectedMessage,
1010
+ );
1011
+ });
1012
+ });
1013
+
686
1014
  async function createCheckpointDirectory(): Promise<string> {
687
1015
  checkpointDirectory = await mkdtemp(join(tmpdir(), "apps-step-checkpoints-"));
688
1016
  process.env.NOTION_WORKFLOW_STEP_DIRECTORY = checkpointDirectory;
@@ -743,23 +1071,54 @@ describe("workflow", () => {
743
1071
  name: "Schedule Follow-up",
744
1072
  description: "Schedules a follow-up after a page is created",
745
1073
  triggers: [triggers.notionPageCreated()],
746
- connections: [connections.calendar()],
1074
+ connections: { calendar: connections.calendar() },
747
1075
  handler: () => {},
748
1076
  });
749
1077
 
750
- expect(definition.config.connections).toEqual([{ type: "calendar" }]);
1078
+ expect(definition.config.connections).toEqual([{ type: "calendar", key: "calendar" }]);
1079
+ });
1080
+
1081
+ it("keeps declaration keys stable after the input object changes", async () => {
1082
+ const declarations = { support: connections.slack() };
1083
+ const seen: string[][] = [];
1084
+ const definition = workflow({
1085
+ name: "Snapshot connections",
1086
+ description: "Captures declared keys",
1087
+ connections: declarations,
1088
+ triggers: [triggers.notionPageCreated()],
1089
+ handler(_event, context) {
1090
+ seen.push(Object.keys(context.connections));
1091
+ },
1092
+ });
1093
+ Object.assign(declarations, { support: connections.calendar(), team: connections.slack() });
1094
+ expect(definition.config.connections).toEqual([{ type: "slack", key: "support" }]);
1095
+ await definition.handler(event, { concreteOutput: true });
1096
+ expect(seen).toEqual([["support"]]);
1097
+ });
1098
+ it("rejects inherited declaration objects before cloning", () => {
1099
+ const declarations = { support: connections.slack() };
1100
+ Object.setPrototypeOf(declarations, { extra: connections.calendar() });
1101
+ expect(() =>
1102
+ workflow({
1103
+ name: "Inherited keys",
1104
+ description: "Reject prototype declarations",
1105
+ connections: declarations,
1106
+ triggers: [triggers.notionPageCreated()],
1107
+ handler() {},
1108
+ }),
1109
+ ).toThrow("only own");
751
1110
  });
752
1111
 
753
- it("rejects duplicate connection requirements", () => {
1112
+ it("rejects invalid connection property names", () => {
754
1113
  expect(() =>
755
1114
  workflow({
756
1115
  name: "Schedule Follow-up",
757
1116
  description: "Schedules a follow-up after a page is created",
758
1117
  triggers: [triggers.notionPageCreated()],
759
- connections: [connections.calendar(), connections.calendar()],
1118
+ connections: { "bad/key": connections.calendar() },
760
1119
  handler: () => {},
761
1120
  }),
762
- ).toThrowError("Duplicate workflow connection: calendar");
1121
+ ).toThrowError("Invalid workflow connection key: bad/key");
763
1122
  });
764
1123
 
765
1124
  it("writes a success envelope after the handler runs", async () => {
@@ -839,7 +1198,7 @@ describe("connection-bound triggers", () => {
839
1198
  const definition = workflow({
840
1199
  name: "Typed triggers",
841
1200
  description: "Uses only declared provider keys",
842
- connections: [connections.slack({ key: "support" }), connections.calendar()],
1201
+ connections: { support: connections.slack(), calendar: connections.calendar() },
843
1202
  triggers: ({ triggers }) => {
844
1203
  calls += 1;
845
1204
  expectTypeOf(triggers.slackMessage)
@@ -861,7 +1220,7 @@ describe("connection-bound triggers", () => {
861
1220
  "slack.message" | "calendar.event.created"
862
1221
  >();
863
1222
  expectTypeOf<keyof typeof context.connections>().toEqualTypeOf<
864
- "slack" | "calendar"
1223
+ "support" | "calendar"
865
1224
  >();
866
1225
  },
867
1226
  });
@@ -876,7 +1235,7 @@ describe("connection-bound triggers", () => {
876
1235
  const definition = workflow({
877
1236
  name: "Support",
878
1237
  description: "Watch support messages",
879
- connections: [connections.slack({ key: "support" })],
1238
+ connections: { support: connections.slack() },
880
1239
  triggers: [triggers.slackMessage({ connectionKey: "support" })],
881
1240
  handler(event) {
882
1241
  expectTypeOf(event.type).toEqualTypeOf<"slack.message">();
@@ -887,9 +1246,9 @@ describe("connection-bound triggers", () => {
887
1246
  ]);
888
1247
  });
889
1248
  it.each([
890
- { requirements: [] },
891
- { requirements: [connections.calendar({ key: "support" })] },
892
- { requirements: [connections.slack({ key: "other" })] },
1249
+ { requirements: {} },
1250
+ { requirements: { support: connections.calendar() } },
1251
+ { requirements: { other: connections.slack() } },
893
1252
  ])("rejects missing or incompatible declarations: %j", ({ requirements }) => {
894
1253
  expect(() =>
895
1254
  workflow({
@@ -905,10 +1264,7 @@ describe("connection-bound triggers", () => {
905
1264
  const configuration = {
906
1265
  name: "Support",
907
1266
  description: "",
908
- connections: [
909
- connections.slack({ key: "support" }),
910
- connections.slack({ key: "internal" }),
911
- ],
1267
+ connections: { support: connections.slack(), internal: connections.slack() },
912
1268
  handler() {},
913
1269
  };
914
1270
  expect(() =>