@get-dx/cli 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { Command, Option } from "commander";
2
+ import { attributeGroupsCommand } from "./commands/attributeGroups.js";
2
3
  import { authCommand } from "./commands/auth.js";
3
4
  import { catalogCommand } from "./commands/catalog.js";
5
+ import { doctorCommand } from "./commands/doctor.js";
4
6
  import { initCommand } from "./commands/init.js";
5
7
  import { scorecardsCommand } from "./commands/scorecards.js";
6
8
  import { snapshotsCommand } from "./commands/snapshots.js";
@@ -48,8 +50,10 @@ function createProgram() {
48
50
  .addOption(new Option("--agent <name>", "Agent name to send as an HTTP header"))
49
51
  .addOption(new Option("--agent-model <model>", "Agent model name to send as an HTTP header"))
50
52
  .addOption(new Option("--agent-session-id <id>", "Agent session ID to send as an HTTP header"));
53
+ program.addCommand(attributeGroupsCommand());
51
54
  program.addCommand(authCommand());
52
55
  program.addCommand(catalogCommand());
56
+ program.addCommand(doctorCommand());
53
57
  program.addCommand(initCommand());
54
58
  program.addCommand(scorecardsCommand());
55
59
  program.addCommand(snapshotsCommand());
@@ -0,0 +1,100 @@
1
+ import { Command } from "commander";
2
+ import { createExampleText, getContext, wrapAction, } from "../commandHelpers.js";
3
+ import { request } from "../http.js";
4
+ import { renderJson, renderRichText } from "../renderers.js";
5
+ import { buildRuntime } from "../runtime.js";
6
+ import * as ui from "../ui.js";
7
+ export function attributeGroupsCommand() {
8
+ const attributeGroups = new Command()
9
+ .name("attributeGroups")
10
+ .description("List attribute groups and values for filtering snapshot scores");
11
+ attributeGroups
12
+ .command("list")
13
+ .description("List attribute groups and their attribute values")
14
+ .addHelpText("afterAll", createExampleText([
15
+ {
16
+ label: "List attribute groups in human-readable output",
17
+ command: "dx attributeGroups list",
18
+ },
19
+ {
20
+ label: "Print raw JSON for automation",
21
+ command: "dx --json attributeGroups list",
22
+ },
23
+ ]))
24
+ .action(wrapAction(async (_options, command) => {
25
+ const runtime = await buildRuntime(getContext(command));
26
+ const response = await listAttributeGroups(runtime);
27
+ if (runtime.context.json) {
28
+ renderJson(response);
29
+ }
30
+ else {
31
+ renderAttributeGroups(response.attribute_groups);
32
+ }
33
+ }));
34
+ return attributeGroups;
35
+ }
36
+ export async function listAttributeGroups(runtime) {
37
+ const response = await request(runtime, "/attributeGroups.list", {
38
+ method: "GET",
39
+ });
40
+ return response.body;
41
+ }
42
+ function renderAttributeGroups(unsortedGroups) {
43
+ const attributeGroups = [...unsortedGroups].sort(byPosition);
44
+ const blocks = [ui.h1("Attribute Groups")];
45
+ blocks.push(ui.p(`Displaying ${ui.bold(attributeGroups.length.toString())} attribute groups.`));
46
+ if (attributeGroups.length === 0) {
47
+ blocks.push(ui.p("No attribute groups are available for this account. Create or sync attribute groups in DX before filtering snapshot scores by attribute."));
48
+ renderRichText(blocks);
49
+ return;
50
+ }
51
+ for (const attributeGroup of attributeGroups) {
52
+ blocks.push(ui.h2(`${attributeGroup.name} (${ui.code(attributeGroup.id)})`));
53
+ blocks.push(ui.dl([
54
+ ui.dli("Type", formatAttributeGroupType(attributeGroup.type)),
55
+ ui.dli("ID", ui.code(attributeGroup.id)),
56
+ ...(attributeGroup.position === null
57
+ ? []
58
+ : [ui.dli("Position", attributeGroup.position.toString())]),
59
+ ...(attributeGroup.hidden ? [ui.dli("Hidden", "Yes")] : []),
60
+ // The API only returns privileged groups the user can see.
61
+ ...(attributeGroup.privileged ? [ui.dli("Privileged", "Yes")] : []),
62
+ ], { termWidth: 12 }));
63
+ if (attributeGroup.attributes.length === 0) {
64
+ blocks.push(ui.p(`Attributes: ${ui.dim("(None)")}`));
65
+ continue;
66
+ }
67
+ blocks.push(ui.p("Attributes:"));
68
+ blocks.push(ui.ul([...attributeGroup.attributes]
69
+ .sort(byPositionThenName)
70
+ .map((attribute) => ui.li(`${attribute.name} (${ui.code(attribute.id)})`))));
71
+ }
72
+ renderRichText(blocks);
73
+ }
74
+ // Items without a position sort last. Ties keep the API's order (a stable
75
+ // sort), which is by id for groups.
76
+ function byPosition(a, b) {
77
+ if (a.position === b.position)
78
+ return 0;
79
+ if (a.position === null)
80
+ return 1;
81
+ if (b.position === null)
82
+ return -1;
83
+ return a.position - b.position;
84
+ }
85
+ // Attributes are ordered by position, then name.
86
+ function byPositionThenName(a, b) {
87
+ return byPosition(a, b) || a.name.localeCompare(b.name);
88
+ }
89
+ function formatAttributeGroupType(type) {
90
+ switch (type.toUpperCase()) {
91
+ case "ADMIN_MANAGED":
92
+ return "Admin managed";
93
+ case "SELF_REPORTED":
94
+ return "Self reported";
95
+ case "DX_MANAGED":
96
+ return "DX managed";
97
+ default:
98
+ return type;
99
+ }
100
+ }
@@ -154,8 +154,9 @@ export function authCommand() {
154
154
  return auth;
155
155
  }
156
156
  // TODO: should we move this somewhere more central, since `init` needs it?
157
- export async function getAuthInfo(runtime) {
157
+ export async function getAuthInfo(runtime, options) {
158
158
  const response = await request(runtime, "/auth.info", {
159
+ ...options,
159
160
  method: "GET",
160
161
  });
161
162
  return response.body;
@@ -14,9 +14,9 @@ export function entityTypesCommand() {
14
14
  .description("Manage catalog entity types");
15
15
  entityTypes
16
16
  .command("create")
17
- .description("Create a new entity type from a YAML file or stdin. The `init` command can be used to generate a starting template.")
18
- .option("--from-file <path>", "Read a YAML file and create an entity type from its contents")
19
- .option("--from-stdin", "Read YAML from stdin and create an entity type from its contents")
17
+ .description("Create a new entity type from a YAML or JSON file or stdin. The `init` command can be used to generate a starting template.")
18
+ .option("--from-file <path>", "Read a YAML or JSON file and create an entity type from its contents")
19
+ .option("--from-stdin", "Read YAML or JSON from stdin and create an entity type from its contents")
20
20
  .addHelpText("afterAll", createExampleText([
21
21
  {
22
22
  label: "Generate a blank template first",
@@ -205,10 +205,10 @@ export function entityTypesCommand() {
205
205
  }));
206
206
  entityTypes
207
207
  .command("update")
208
- .description("Update an existing entity type from a YAML file or stdin. The `init` command can be used to initialize the entity type file.")
208
+ .description("Update an existing entity type from a YAML or JSON file or stdin. The `init` command can be used to initialize the entity type file.")
209
209
  .argument("<identifier>", "The unique identifier of the entity type")
210
- .option("--from-file <path>", "Read a YAML file and update the entity type with its contents")
211
- .option("--from-stdin", "Read YAML from stdin and update the entity type with its contents")
210
+ .option("--from-file <path>", "Read a YAML or JSON file and update the entity type with its contents")
211
+ .option("--from-stdin", "Read YAML or JSON from stdin and update the entity type with its contents")
212
212
  .addHelpText("afterAll", createExampleText([
213
213
  {
214
214
  label: "Initialize a file first",
@@ -0,0 +1,85 @@
1
+ import { Command } from "commander";
2
+ import { createExampleText, getContext, wrapAction, } from "../commandHelpers.js";
3
+ import { runDoctor } from "../doctor/index.js";
4
+ import { AsyncProgressReporter, renderJson, renderRichText, } from "../renderers.js";
5
+ import * as ui from "../ui.js";
6
+ const MIN_LABEL_WIDTH = 26;
7
+ const PHASE_MESSAGES = {
8
+ config: "Checking configuration…",
9
+ network: "Checking network…",
10
+ auth: "Checking authentication…",
11
+ version: "Checking for updates…",
12
+ };
13
+ export function doctorCommand() {
14
+ return new Command("doctor")
15
+ .description("Diagnose problems with your CLI instance's installation, configuration, networking, and authentication")
16
+ .addHelpText("afterAll", createExampleText([
17
+ { label: "Check your setup", command: "dx doctor" },
18
+ { label: "Print the report as JSON", command: "dx doctor --json" },
19
+ ]))
20
+ .action(wrapAction(async (_options, command) => {
21
+ const context = getContext(command);
22
+ const progress = new AsyncProgressReporter();
23
+ progress.start(PHASE_MESSAGES.config);
24
+ let report;
25
+ try {
26
+ report = await runDoctor(context, {
27
+ onPhase: (phase) => progress.update(PHASE_MESSAGES[phase]),
28
+ });
29
+ }
30
+ finally {
31
+ progress.stop();
32
+ }
33
+ if (context.json) {
34
+ renderJson(report);
35
+ }
36
+ else {
37
+ renderDoctorReport(report);
38
+ }
39
+ if (!report.ok) {
40
+ process.exitCode = 1;
41
+ }
42
+ }));
43
+ }
44
+ export function renderDoctorReport(report) {
45
+ const labelWidth = Math.max(MIN_LABEL_WIDTH, ...report.sections.flatMap((section) => section.checks.map((check) => check.label.length + 2)));
46
+ renderRichText([
47
+ ...report.sections.map((section) => ui.statusList(section.title, section.checks.map((check) => ({
48
+ status: check.status,
49
+ label: check.label,
50
+ detail: formatDetail(check),
51
+ hint: check.hint,
52
+ })), { labelWidth })),
53
+ ui.p(summaryLine(report)),
54
+ ]);
55
+ }
56
+ function formatDetail(check) {
57
+ if (!check.source) {
58
+ return check.message;
59
+ }
60
+ return `${check.message} ${ui.dim(`[from ${sourceName(check.source)}]`)}`;
61
+ }
62
+ function sourceName(source) {
63
+ switch (source) {
64
+ case "env":
65
+ return "env";
66
+ case "config":
67
+ return "config file";
68
+ case "default":
69
+ return "default";
70
+ case "keychain":
71
+ return "keychain";
72
+ }
73
+ }
74
+ function summaryLine(report) {
75
+ const { fail, warn } = report.summary;
76
+ const plural = (count, word) => `${count} ${word}${count === 1 ? "" : "s"}`;
77
+ if (fail > 0) {
78
+ const failed = ui.error(`${plural(fail, "check")} failed`);
79
+ return warn > 0 ? `${failed}, ${plural(warn, "warning")}.` : `${failed}.`;
80
+ }
81
+ if (warn > 0) {
82
+ return ui.warning(`All required checks passed, ${plural(warn, "warning")}.`);
83
+ }
84
+ return ui.success("All checks passed.");
85
+ }
@@ -14,9 +14,9 @@ export function scorecardsCommand() {
14
14
  .description("Manage scorecards");
15
15
  scorecards
16
16
  .command("create")
17
- .description("Create a new scorecard from a YAML file or stdin. The `init` command can be used to generate a starting template.")
18
- .option("--from-file <path>", "Read a YAML file and create a scorecard from its contents")
19
- .option("--from-stdin", "Read YAML from stdin and create a scorecard from its contents")
17
+ .description("Create a new scorecard from a YAML or JSON file or stdin. The `init` command can be used to generate a starting template.")
18
+ .option("--from-file <path>", "Read a YAML or JSON file and create a scorecard from its contents")
19
+ .option("--from-stdin", "Read YAML or JSON from stdin and create a scorecard from its contents")
20
20
  .addHelpText("afterAll", createExampleText([
21
21
  {
22
22
  label: "Generate a blank template first",
@@ -216,10 +216,10 @@ export function scorecardsCommand() {
216
216
  }));
217
217
  scorecards
218
218
  .command("update")
219
- .description("Update an existing scorecard from a YAML file or stdin. The `init` command can be used to initialize the scorecard file.")
219
+ .description("Update an existing scorecard from a YAML or JSON file or stdin. The `init` command can be used to initialize the scorecard file.")
220
220
  .argument("<id>", "The unique ID of the scorecard")
221
- .option("--from-file <path>", "Read a YAML file and update the scorecard with its contents")
222
- .option("--from-stdin", "Read YAML from stdin and update the scorecard with its contents")
221
+ .option("--from-file <path>", "Read a YAML or JSON file and update the scorecard with its contents")
222
+ .option("--from-stdin", "Read YAML or JSON from stdin and update the scorecard with its contents")
223
223
  .addHelpText("afterAll", createExampleText([
224
224
  {
225
225
  label: "Initialize a file first",
@@ -4,6 +4,7 @@ import { CliError, EXIT_CODES } from "../errors.js";
4
4
  import { request } from "../http.js";
5
5
  import { renderJson, renderRichText } from "../renderers.js";
6
6
  import { buildRuntime } from "../runtime.js";
7
+ import { formatTeamMember } from "./teams.js";
7
8
  import * as ui from "../ui.js";
8
9
  export function snapshotsCommand() {
9
10
  const snapshots = new Command()
@@ -135,6 +136,35 @@ export function snapshotsCommand() {
135
136
  renderSnapshots(response.snapshots, runtime);
136
137
  }
137
138
  }));
139
+ const snapshotTeams = new Command()
140
+ .name("snapshotTeams")
141
+ .description("Work with snapshot teams");
142
+ snapshotTeams
143
+ .command("list")
144
+ .description("List teams for a snapshot")
145
+ .option("--id <id>", "The unique ID of the snapshot")
146
+ .addHelpText("afterAll", createExampleText([
147
+ {
148
+ label: "List teams for a snapshot",
149
+ command: "dx snapshots snapshotTeams list --id MjUyNbaY",
150
+ },
151
+ {
152
+ label: "List snapshot teams as JSON",
153
+ command: "dx --json snapshots snapshotTeams list --id MjUyNbaY",
154
+ },
155
+ ]))
156
+ .action(wrapAction(async (options, command) => {
157
+ const snapshotId = parseRequiredTextOption(options.id, "--id");
158
+ const runtime = await buildRuntime(getContext(command));
159
+ const response = await listSnapshotTeams(runtime, snapshotId);
160
+ if (runtime.context.json) {
161
+ renderJson(response);
162
+ }
163
+ else {
164
+ renderSnapshotTeams(response.snapshot_teams, snapshotId, runtime);
165
+ }
166
+ }));
167
+ snapshots.addCommand(snapshotTeams);
138
168
  return snapshots;
139
169
  }
140
170
  async function listSnapshotDriverComments(runtime, options) {
@@ -159,6 +189,13 @@ async function listSnapshotCsatComments(runtime, options) {
159
189
  });
160
190
  return response.body;
161
191
  }
192
+ async function listSnapshotTeams(runtime, snapshotId) {
193
+ const response = await request(runtime, "/snapshots.snapshotTeams.list", {
194
+ method: "GET",
195
+ query: { snapshot_id: snapshotId },
196
+ });
197
+ return response.body;
198
+ }
162
199
  async function getSnapshotInfo(runtime, snapshotId) {
163
200
  const response = await request(runtime, "/snapshots.info", {
164
201
  method: "GET",
@@ -237,6 +274,22 @@ function renderSnapshotCsatComments(comments, nextCursor, snapshotId, runtime) {
237
274
  }
238
275
  renderRichText(blocks);
239
276
  }
277
+ function renderSnapshotTeams(teams, snapshotId, runtime) {
278
+ const blocks = [ui.h1("Snapshot Teams")];
279
+ blocks.push(ui.p(`Displaying ${ui.bold(teams.length.toString())} teams.`));
280
+ blocks.push(ui.p(`Snapshot Team IDs are only valid within this snapshot and feed ${ui.code("snapshot_team_ids")} score filters. Use ${ui.code("team_id")} with ${ui.code("dx teams info --team-id")}.`));
281
+ blocks.push(ui.p(ui.link(snapshotWebLink(snapshotId, "drivers", runtime))));
282
+ if (teams.length === 0) {
283
+ blocks.push(ui.p(ui.dim("(None)")));
284
+ renderRichText(blocks);
285
+ return;
286
+ }
287
+ const rootTeams = buildSnapshotTeamTree(teams);
288
+ rootTeams.forEach((rootTeam) => {
289
+ appendSnapshotTeamBlock(blocks, rootTeam, 0);
290
+ });
291
+ renderRichText(blocks);
292
+ }
240
293
  function renderSnapshotInfo(snapshot, snapshotId, runtime) {
241
294
  const teamScores = snapshot.team_scores ?? [];
242
295
  const blocks = [ui.h1("Snapshot Information")];
@@ -298,6 +351,69 @@ function extractCsatComments(response) {
298
351
  response.comments ??
299
352
  []);
300
353
  }
354
+ class SnapshotTeamTreeNode {
355
+ team;
356
+ children = [];
357
+ constructor(team) {
358
+ this.team = team;
359
+ }
360
+ }
361
+ function buildSnapshotTeamTree(teams) {
362
+ const nodes = new Map();
363
+ teams.forEach((team) => {
364
+ nodes.set(team.id, new SnapshotTeamTreeNode(team));
365
+ });
366
+ const rootTeams = [];
367
+ teams.forEach((team) => {
368
+ const currentNode = nodes.get(team.id);
369
+ if (currentNode.team.parent_id === null) {
370
+ // It's a top-level root node
371
+ rootTeams.push(currentNode);
372
+ }
373
+ else {
374
+ // Append it to its parent's children array
375
+ nodes.get(currentNode.team.parent_id).children.push(currentNode);
376
+ }
377
+ });
378
+ const sortNodes = (items) => {
379
+ items.sort((left, right) => compareSnapshotTeams(left.team, right.team));
380
+ for (const item of items) {
381
+ sortNodes(item.children);
382
+ }
383
+ };
384
+ sortNodes(rootTeams);
385
+ return rootTeams;
386
+ }
387
+ function compareSnapshotTeams(left, right) {
388
+ const leftName = left.name.toLocaleLowerCase();
389
+ const rightName = right.name.toLocaleLowerCase();
390
+ if (leftName !== rightName) {
391
+ return leftName.localeCompare(rightName);
392
+ }
393
+ // If the names are the same, compare the IDs
394
+ const leftId = left.id.toLocaleLowerCase();
395
+ const rightId = right.id.toLocaleLowerCase();
396
+ return leftId.localeCompare(rightId);
397
+ }
398
+ function appendSnapshotTeamBlock(blocks, node, depth) {
399
+ blocks.push(depth === 0
400
+ ? ui.h2(node.team.name)
401
+ : ui.h3(`${"↳".repeat(depth)} ${node.team.name}`));
402
+ const details = [
403
+ optionalDetail("Snapshot Team ID", formatText(node.team.id), node.team.id),
404
+ optionalDetail("Team ID", formatText(node.team.team_id), node.team.team_id),
405
+ // Match `dx teams info`: show "(None)" when the API returns a null manager,
406
+ // omit the row only when the field is absent entirely.
407
+ ui.dli("Manager", formatTeamMember(node.team.manager)),
408
+ optionalDetail("Parent ID", formatText(node.team.parent_id), node.team.parent_id),
409
+ ].filter((detail) => detail !== null);
410
+ if (details.length > 0) {
411
+ blocks.push(ui.dl(details, { termWidth: 18 }));
412
+ }
413
+ node.children.forEach((child) => {
414
+ appendSnapshotTeamBlock(blocks, child, depth + 1);
415
+ });
416
+ }
301
417
  function formatCommentHeading(comment) {
302
418
  const heading = comment.item_name ??
303
419
  comment.driver_name ??
@@ -17,9 +17,9 @@ export function reportsCommand() {
17
17
  .description("Manage Data Studio reports");
18
18
  reports
19
19
  .command("create")
20
- .description("Create a Data Studio report from a YAML file or stdin")
21
- .option("--from-file <path>", "Read a YAML file and create a report from its contents")
22
- .option("--from-stdin", "Read YAML from stdin and create a report from its contents")
20
+ .description("Create a Data Studio report from a YAML or JSON file or stdin")
21
+ .option("--from-file <path>", "Read a YAML or JSON file and create a report from its contents")
22
+ .option("--from-stdin", "Read YAML or JSON from stdin and create a report from its contents")
23
23
  .addHelpText("afterAll", createNoteText(VARIABLES_NOTE_TEXT))
24
24
  .addHelpText("afterAll", createExampleText([
25
25
  {
@@ -175,10 +175,10 @@ export function reportsCommand() {
175
175
  }));
176
176
  reports
177
177
  .command("update")
178
- .description("Update a Data Studio report from a YAML file or stdin. The `init` command can be used to initialize the report file.")
178
+ .description("Update a Data Studio report from a YAML or JSON file or stdin. The `init` command can be used to initialize the report file.")
179
179
  .argument("<id>", "Studio report ID")
180
- .option("--from-file <path>", "Read a YAML file and update the report with its contents")
181
- .option("--from-stdin", "Read YAML from stdin and update the report with its contents")
180
+ .option("--from-file <path>", "Read a YAML or JSON file and update the report with its contents")
181
+ .option("--from-stdin", "Read YAML or JSON from stdin and update the report with its contents")
182
182
  .addHelpText("afterAll", createNoteText(VARIABLES_NOTE_TEXT))
183
183
  .addHelpText("afterAll", createExampleText([
184
184
  {
@@ -200,7 +200,7 @@ function formatTeamHeading(team) {
200
200
  }
201
201
  return null;
202
202
  }
203
- function formatTeamMember(member) {
203
+ export function formatTeamMember(member) {
204
204
  if (!member) {
205
205
  return ui.dim("(None)");
206
206
  }
@@ -0,0 +1,78 @@
1
+ # Self-service workflow configuration
2
+ #
3
+ # Edit this file, then run:
4
+ #
5
+ # ```
6
+ # dx workflows create --from-file <path>
7
+ # ```
8
+
9
+ # Unique per account. Letters, numbers, ".", "_" and "-" only; "new" is reserved.
10
+ identifier: ""
11
+ name: ""
12
+ description: ""
13
+ # icon: globe
14
+ color: "#38bdf8"
15
+
16
+ # scope: "GLOBAL" (not tied to a catalog entity) or "ENTITY" (run against an entity)
17
+ scope: GLOBAL
18
+
19
+ # execution_type: "SIMPLE" or "EVENT_DRIVEN"
20
+ execution_type: SIMPLE
21
+
22
+ # Who can trigger a run:
23
+ # ANY - all users, no approval
24
+ # APPROVAL - all users, but an approver must accept (set approver_user_ids)
25
+ # RUNNERS - only selected users and teams (set runners)
26
+ # ENTITY - only owners of the entity (requires scope: ENTITY)
27
+ trigger_type: ANY
28
+ # approver_user_ids: ["<active user id>"] # at least one when APPROVAL
29
+ # runners: # at least one when RUNNERS
30
+ # - user_id: "<user id>" # each runner sets exactly one of
31
+ # - squad_id: "<team id>" # user_id or squad_id
32
+ # squad_linkage_id: null # optional
33
+
34
+ # Which entities the workflow can run against (only used when scope is ENTITY).
35
+ # entity_filter_type: "ENTITY_TYPES" or "SQL"
36
+ entity_filter_type: ENTITY_TYPES
37
+ # Entity type identifiers (when entity_filter_type is "ENTITY_TYPES"; at least
38
+ # one is required when scope is ENTITY)
39
+ entity_filter_type_identifiers: []
40
+ # SQL filter expression (required when scope is ENTITY and entity_filter_type is "SQL")
41
+ # entity_filter_sql: ""
42
+
43
+ # The HTTP request sent when the workflow runs.
44
+ # The url, body, header values and form_data values can use variables:
45
+ # {{run.id}}, {{workflow.identifier}}, {{workflow.name}},
46
+ # {{triggered_by_user.id}}, {{triggered_by_user.name}}, {{triggered_by_user.email}},
47
+ # {{triggered_by_user.team_id}}, {{triggered_by_user.team_name}},
48
+ # {{entity.identifier}}, {{entity.name}} (scope: ENTITY only),
49
+ # {{data.<parameter identifier>}}, {{secret.<name>}}
50
+ http_request:
51
+ # method: GET, POST, PUT, PATCH or DELETE
52
+ method: POST
53
+ url: "https://example.com/hooks/my-workflow"
54
+ headers:
55
+ Content-Type: application/json
56
+ # A string or null. Must be null when form_data is used.
57
+ body: |-
58
+ {"run_id": "{{run.id}}"}
59
+ # form_data:
60
+ # - key: example
61
+ # value: "{{data.example}}"
62
+
63
+ # Inputs collected from the user when triggering a run. Each parameter needs an
64
+ # identifier (letters, numbers, underscores), a name, a type and is_required.
65
+ parameters: []
66
+ # parameters:
67
+ # - identifier: environment
68
+ # name: Environment
69
+ # description: ""
70
+ # # type: STRING, INTEGER, FLOAT, BOOLEAN, SELECT, USER, TEAM, EMAIL or COMPUTED
71
+ # type: SELECT
72
+ # is_required: true
73
+ # default_value: staging
74
+ # # SELECT: {options: [...]} COMPUTED: {sql: "..."} other types: omit
75
+ # definition:
76
+ # options:
77
+ # - staging
78
+ # - production