@mindstudio-ai/agent 0.1.58 → 0.1.60

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
@@ -473,13 +473,6 @@ var init_metadata = __esm({
473
473
  inputSchema: { "type": "object", "properties": { "mediaUrl": { "type": "string", "description": "URL of the audio or video file to analyze" } }, "required": ["mediaUrl"] },
474
474
  outputSchema: { "type": "object", "properties": { "metadata": { "type": "string", "description": "JSON string containing the media file metadata" } }, "required": ["metadata"] }
475
475
  },
476
- "httpRequest": {
477
- stepType: "httpRequest",
478
- description: "Make an HTTP request to an external endpoint and return the response.",
479
- usageNotes: "- Supports GET, POST, PATCH, DELETE, and PUT methods.\n- Body can be raw JSON/text, URL-encoded form data, or multipart form data.",
480
- inputSchema: { "type": "object", "properties": { "url": { "type": "string", "description": "The request URL" }, "method": { "type": "string", "description": "HTTP method (GET, POST, PATCH, DELETE, or PUT)" }, "headers": { "type": "object", "properties": {}, "required": [], "description": "Custom request headers as key-value pairs" }, "queryParams": { "type": "object", "properties": {}, "required": [], "description": "Query string parameters as key-value pairs" }, "body": { "type": "string", "description": "Raw request body (used for JSON or custom content types)" }, "bodyItems": { "type": "object", "properties": {}, "required": [], "description": "Key-value body items (used for form data or URL-encoded content types)" }, "contentType": { "enum": ["none", "application/json", "application/x-www-form-urlencoded", "multipart/form-data", "custom"], "type": "string", "description": "The content type for the request body" }, "customContentType": { "type": "string", "description": 'Custom Content-Type header value (used when contentType is "custom")' }, "testData": { "type": "object", "properties": {}, "required": [], "description": "Test data for debug/preview mode" } }, "required": ["url", "method", "headers", "queryParams", "body", "bodyItems", "contentType", "customContentType"], "description": "HTTP request configuration" },
481
- outputSchema: { "type": "object", "properties": { "ok": { "type": "boolean", "description": "Whether the HTTP response status code is in the 2xx range" }, "status": { "type": "number", "description": "HTTP response status code" }, "statusText": { "type": "string", "description": "HTTP response status text" }, "response": { "type": "string", "description": "Response body as a string" } }, "required": ["ok", "status", "statusText", "response"] }
482
- },
483
476
  "hubspotCreateCompany": {
484
477
  stepType: "hubspotCreateCompany",
485
478
  description: "Create a new company or update an existing one in HubSpot. Matches by domain.",
@@ -1521,7 +1514,7 @@ function serializeParam(val) {
1521
1514
  function serializeColumnParam(val, columnName, columns) {
1522
1515
  const col = columns.find((c) => c.name === columnName);
1523
1516
  if (col?.type === "user" && typeof val === "string") {
1524
- return `@@user@@${val}`;
1517
+ return val.startsWith(USER_PREFIX) ? val : `${USER_PREFIX}${val}`;
1525
1518
  }
1526
1519
  return serializeParam(val);
1527
1520
  }
@@ -1538,7 +1531,7 @@ function deserializeRow(row, columns) {
1538
1531
  const result = {};
1539
1532
  for (const [key, value] of Object.entries(row)) {
1540
1533
  const col = columns.find((c) => c.name === key);
1541
- if (col?.type === "user" && typeof value === "string" && value.startsWith(USER_PREFIX)) {
1534
+ if (typeof value === "string" && value.startsWith(USER_PREFIX)) {
1542
1535
  result[key] = value.slice(USER_PREFIX.length);
1543
1536
  } else if (col?.type === "json" && typeof value === "string") {
1544
1537
  try {
@@ -1648,16 +1641,16 @@ var init_sql = __esm({
1648
1641
  });
1649
1642
 
1650
1643
  // src/db/predicate.ts
1651
- function compilePredicate(fn) {
1644
+ function compilePredicate(fn, bindings) {
1652
1645
  try {
1653
1646
  const source = fn.toString();
1654
- const paramName = extractParamName(source);
1655
- if (!paramName) return { type: "js", fn, reason: "could not extract parameter name" };
1647
+ const names = extractParamNames(source);
1648
+ if (!names) return { type: "js", fn, reason: "could not extract parameter name" };
1656
1649
  const body = extractBody(source);
1657
1650
  if (!body) return { type: "js", fn, reason: "could not extract function body" };
1658
1651
  const tokens = tokenize(body);
1659
1652
  if (tokens.length === 0) return { type: "js", fn, reason: "empty token stream" };
1660
- const parser = new Parser(tokens, paramName, fn);
1653
+ const parser = new Parser(tokens, names.row, names.bindings, bindings, fn);
1661
1654
  const ast = parser.parseExpression();
1662
1655
  if (!ast) return { type: "js", fn, reason: "could not parse expression" };
1663
1656
  if (parser.pos < tokens.length) return { type: "js", fn, reason: "unexpected tokens after expression" };
@@ -1668,11 +1661,44 @@ function compilePredicate(fn) {
1668
1661
  return { type: "js", fn, reason: `compilation error: ${err?.message || "unknown"}` };
1669
1662
  }
1670
1663
  }
1671
- function extractParamName(source) {
1672
- const match = source.match(
1673
- /^\s*(?:\(?\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?::[^)]*?)?\)?\s*=>)/
1674
- );
1675
- return match?.[1] ?? null;
1664
+ function extractParamNames(source) {
1665
+ const arrowIdx = source.indexOf("=>");
1666
+ if (arrowIdx === -1) return null;
1667
+ let paramList = source.slice(0, arrowIdx).trim();
1668
+ if (paramList.startsWith("(") && paramList.endsWith(")")) {
1669
+ paramList = paramList.slice(1, -1).trim();
1670
+ }
1671
+ if (paramList.length === 0) return null;
1672
+ const parts = splitParams(paramList);
1673
+ if (parts.length === 0 || parts.length > 2) return null;
1674
+ const row = stripTypeAnnotation(parts[0]);
1675
+ if (!row) return null;
1676
+ if (parts.length === 1) return { row };
1677
+ const bindings = stripTypeAnnotation(parts[1]);
1678
+ if (!bindings) return null;
1679
+ return { row, bindings };
1680
+ }
1681
+ function splitParams(input) {
1682
+ const parts = [];
1683
+ let depth = 0;
1684
+ let current = "";
1685
+ for (const ch of input) {
1686
+ if (ch === "<" || ch === "(") depth++;
1687
+ else if (ch === ">" || ch === ")") depth--;
1688
+ if (ch === "," && depth === 0) {
1689
+ parts.push(current.trim());
1690
+ current = "";
1691
+ } else {
1692
+ current += ch;
1693
+ }
1694
+ }
1695
+ if (current.trim()) parts.push(current.trim());
1696
+ return parts;
1697
+ }
1698
+ function stripTypeAnnotation(part) {
1699
+ const colonIdx = part.indexOf(":");
1700
+ const name = (colonIdx === -1 ? part : part.slice(0, colonIdx)).trim();
1701
+ return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : null;
1676
1702
  }
1677
1703
  function extractBody(source) {
1678
1704
  const arrowIdx = source.indexOf("=>");
@@ -1821,9 +1847,11 @@ var init_predicate = __esm({
1821
1847
  "use strict";
1822
1848
  init_sql();
1823
1849
  Parser = class {
1824
- constructor(tokens, paramName, originalFn) {
1850
+ constructor(tokens, paramName, bindingsName, bindingsValue, originalFn) {
1825
1851
  this.tokens = tokens;
1826
1852
  this.paramName = paramName;
1853
+ this.bindingsName = bindingsName;
1854
+ this.bindingsValue = bindingsValue;
1827
1855
  this.originalFn = originalFn;
1828
1856
  }
1829
1857
  pos = 0;
@@ -1920,6 +1948,9 @@ var init_predicate = __esm({
1920
1948
  if (this.match("identifier", this.paramName)) {
1921
1949
  return this.parseFieldExpression();
1922
1950
  }
1951
+ if (this.bindingsName && this.match("identifier", this.bindingsName)) {
1952
+ return this.parseBindingsArrayIncludes();
1953
+ }
1923
1954
  if (this.match("identifier")) {
1924
1955
  return this.parseNonParamExpression();
1925
1956
  }
@@ -1956,6 +1987,7 @@ var init_predicate = __esm({
1956
1987
  if (!this.match("identifier")) return null;
1957
1988
  const parts = [this.advance().value];
1958
1989
  while (this.match("dot") && this.tokens[this.pos + 1]?.type === "identifier") {
1990
+ if (this.tokens[this.pos + 2]?.type === "lparen") break;
1959
1991
  this.advance();
1960
1992
  parts.push(this.advance().value);
1961
1993
  }
@@ -2040,6 +2072,68 @@ var init_predicate = __esm({
2040
2072
  if (ident === "true" || ident === "false") return null;
2041
2073
  return null;
2042
2074
  }
2075
+ /**
2076
+ * Parse `$.ids.includes(o.field)` → IN expression with resolved values.
2077
+ * The bindings identifier has been peeked but not consumed.
2078
+ *
2079
+ * Falls back (returns null) if the resolved bindings value isn't an array,
2080
+ * or if the path doesn't exist on the bindings object.
2081
+ */
2082
+ parseBindingsArrayIncludes() {
2083
+ const bound = this.tryResolveBindingsValue();
2084
+ if (bound === PARSE_FAILED) return null;
2085
+ if (!Array.isArray(bound)) return null;
2086
+ if (!this.eat("dot")) return null;
2087
+ if (!this.match("identifier", "includes")) return null;
2088
+ this.advance();
2089
+ if (!this.eat("lparen")) return null;
2090
+ if (!this.match("identifier", this.paramName)) return null;
2091
+ this.advance();
2092
+ const field = this.parseFieldPath();
2093
+ if (!field) return null;
2094
+ if (!this.eat("rparen")) return null;
2095
+ return { kind: "in", field, values: bound };
2096
+ }
2097
+ /**
2098
+ * If the current token is the bindings parameter name, walk a dotted path
2099
+ * (`$.foo.bar`) and resolve the value from the bindings object. Returns
2100
+ * the resolved value or `PARSE_FAILED`.
2101
+ *
2102
+ * Stops walking before a method call (e.g. doesn't consume `.includes` in
2103
+ * `$.ids.includes(...)`) so the caller can dispatch on what follows.
2104
+ *
2105
+ * Restores `pos` on failure so callers can fall through cleanly.
2106
+ */
2107
+ tryResolveBindingsValue() {
2108
+ if (!this.bindingsName) return PARSE_FAILED;
2109
+ if (!this.match("identifier", this.bindingsName)) return PARSE_FAILED;
2110
+ if (this.bindingsValue == null) return PARSE_FAILED;
2111
+ const startPos = this.pos;
2112
+ this.advance();
2113
+ const path = [];
2114
+ while (this.match("dot") && this.tokens[this.pos + 1]?.type === "identifier") {
2115
+ if (this.tokens[this.pos + 2]?.type === "lparen") break;
2116
+ this.advance();
2117
+ path.push(this.advance().value);
2118
+ }
2119
+ if (path.length === 0) {
2120
+ this.pos = startPos;
2121
+ return PARSE_FAILED;
2122
+ }
2123
+ let value = this.bindingsValue;
2124
+ for (const key of path) {
2125
+ if (value == null || typeof value !== "object") {
2126
+ this.pos = startPos;
2127
+ return PARSE_FAILED;
2128
+ }
2129
+ value = value[key];
2130
+ }
2131
+ if (value === void 0) {
2132
+ this.pos = startPos;
2133
+ return PARSE_FAILED;
2134
+ }
2135
+ return value;
2136
+ }
2043
2137
  /**
2044
2138
  * Parse a literal value or closure variable reference.
2045
2139
  *
@@ -2074,6 +2168,10 @@ var init_predicate = __esm({
2074
2168
  this.advance();
2075
2169
  return void 0;
2076
2170
  }
2171
+ if (this.bindingsName && t.value === this.bindingsName) {
2172
+ const bound = this.tryResolveBindingsValue();
2173
+ if (bound !== PARSE_FAILED) return bound;
2174
+ }
2077
2175
  return this.resolveClosureVariable();
2078
2176
  }
2079
2177
  if (t.type === "operator" && t.value === "-") {
@@ -2190,11 +2288,10 @@ var init_query = __esm({
2190
2288
  rawWhereParams: this._rawWhereParams
2191
2289
  });
2192
2290
  }
2193
- // -------------------------------------------------------------------------
2194
- // Chain methods
2195
- // -------------------------------------------------------------------------
2196
- filter(predicate) {
2197
- return this._clone({ predicates: [...this._predicates, predicate] });
2291
+ filter(predicate, bindings) {
2292
+ return this._clone({
2293
+ predicates: [...this._predicates, { fn: predicate, bindings }]
2294
+ });
2198
2295
  }
2199
2296
  sortBy(accessor) {
2200
2297
  return this._clone({ sortAccessor: accessor });
@@ -2251,7 +2348,7 @@ var init_query = __esm({
2251
2348
  if (this._predicates.length === 0) return true;
2252
2349
  const allRows = await this._fetchAllRows();
2253
2350
  return allRows.every(
2254
- (row) => this._predicates.every((pred) => pred(row))
2351
+ (row) => this._predicates.every((p) => p.fn(row, p.bindings))
2255
2352
  );
2256
2353
  }
2257
2354
  min(accessor) {
@@ -2339,7 +2436,7 @@ var init_query = __esm({
2339
2436
  if (compiled.query) {
2340
2437
  return compiled.postProcess ? compiled.postProcess(rows) : rows;
2341
2438
  }
2342
- let filtered = compiled.predicates ? rows.filter((row) => compiled.predicates.every((pred) => pred(row))) : rows;
2439
+ let filtered = compiled.predicates ? rows.filter((row) => compiled.predicates.every((p) => p.fn(row, p.bindings))) : rows;
2343
2440
  if (compiled.sortAccessor) {
2344
2441
  const accessor = compiled.sortAccessor;
2345
2442
  const reversed = compiled.reversed ?? false;
@@ -2429,7 +2526,7 @@ var init_query = __esm({
2429
2526
  if (this._predicates.length === 0) {
2430
2527
  return { allSql: true, sqlWhere: "", compiled: [] };
2431
2528
  }
2432
- const compiled = this._predicates.map((pred) => compilePredicate(pred));
2529
+ const compiled = this._predicates.map((p) => compilePredicate(p.fn, p.bindings));
2433
2530
  const allSql = compiled.every((c) => c.type === "sql");
2434
2531
  let sqlWhere = "";
2435
2532
  if (allSql) {
@@ -2448,7 +2545,7 @@ var init_query = __esm({
2448
2545
  );
2449
2546
  }
2450
2547
  return allRows.filter(
2451
- (row) => this._predicates.every((pred) => pred(row))
2548
+ (row) => this._predicates.every((p) => p.fn(row, p.bindings))
2452
2549
  );
2453
2550
  }
2454
2551
  async _fetchAllRows() {
@@ -2579,21 +2676,18 @@ var init_table = __esm({
2579
2676
  postProcess: (rows) => rows[0] ?? null
2580
2677
  });
2581
2678
  }
2582
- /** Find the first row matching a predicate. Returns null if none match. */
2583
- findOne(predicate) {
2584
- return this.filter(predicate).first();
2679
+ findOne(predicate, bindings) {
2680
+ return this.filter(predicate, bindings).first();
2585
2681
  }
2586
- count(predicate) {
2587
- if (predicate) return this.filter(predicate).count();
2682
+ count(predicate, bindings) {
2683
+ if (predicate) return this.filter(predicate, bindings).count();
2588
2684
  return this.toArray().count();
2589
2685
  }
2590
- /** True if any row matches the predicate. */
2591
- some(predicate) {
2592
- return this.filter(predicate).some();
2686
+ some(predicate, bindings) {
2687
+ return this.filter(predicate, bindings).some();
2593
2688
  }
2594
- /** True if all rows match the predicate. */
2595
- async every(predicate) {
2596
- return this.filter(predicate).every();
2689
+ async every(predicate, bindings) {
2690
+ return this.filter(predicate, bindings).every();
2597
2691
  }
2598
2692
  /** True if the table has zero rows. */
2599
2693
  async isEmpty() {
@@ -2618,9 +2712,8 @@ var init_table = __esm({
2618
2712
  toArray() {
2619
2713
  return new Query(this._config);
2620
2714
  }
2621
- /** Filter rows by a predicate. Returns a chainable Query. */
2622
- filter(predicate) {
2623
- return new Query(this._config).filter(predicate);
2715
+ filter(predicate, bindings) {
2716
+ return new Query(this._config).filter(predicate, bindings);
2624
2717
  }
2625
2718
  /** Sort rows by a field. Returns a chainable Query. */
2626
2719
  sortBy(accessor) {
@@ -2702,11 +2795,8 @@ var init_table = __esm({
2702
2795
  deleted: results[0].changes > 0
2703
2796
  }));
2704
2797
  }
2705
- /**
2706
- * Remove all rows matching a predicate. Returns the count removed.
2707
- */
2708
- removeAll(predicate) {
2709
- const compiled = compilePredicate(predicate);
2798
+ removeAll(predicate, bindings) {
2799
+ const compiled = compilePredicate(predicate, bindings);
2710
2800
  if (compiled.type === "sql") {
2711
2801
  const query = buildDelete(this._config.tableName, compiled.where);
2712
2802
  return new Mutation(this._config, [query], (results) => results[0].changes);
@@ -2723,7 +2813,7 @@ var init_table = __esm({
2723
2813
  this._config.columns
2724
2814
  )
2725
2815
  );
2726
- const matching = allRows.filter((row) => predicate(row));
2816
+ const matching = allRows.filter((row) => predicate(row, bindings));
2727
2817
  if (matching.length === 0) return 0;
2728
2818
  const deleteQueries = matching.filter((row) => row.id).map((row) => buildDelete(this._config.tableName, `id = ?`, [row.id]));
2729
2819
  if (deleteQueries.length > 0) {
@@ -2877,6 +2967,8 @@ function createDb(databases, executeBatch, authConfig, syncRoles) {
2877
2967
  minutes: (n) => n * 6e4,
2878
2968
  ago: (ms) => Date.now() - ms,
2879
2969
  fromNow: (ms) => Date.now() + ms,
2970
+ // --- User references ---
2971
+ userRef: (id) => id.startsWith(USER_PREFIX) ? id.slice(USER_PREFIX.length) : id,
2880
2972
  // --- Batch execution ---
2881
2973
  batch: ((...operations) => {
2882
2974
  return (async () => {
@@ -2995,6 +3087,7 @@ var init_db = __esm({
2995
3087
  init_table();
2996
3088
  init_query();
2997
3089
  init_mutation();
3090
+ init_sql();
2998
3091
  init_table();
2999
3092
  }
3000
3093
  });
@@ -3407,9 +3500,6 @@ function applyStepMethods(AgentClass) {
3407
3500
  proto.getMediaMetadata = function(step, options) {
3408
3501
  return this.executeStep("getMediaMetadata", step, options);
3409
3502
  };
3410
- proto.httpRequest = function(step, options) {
3411
- return this.executeStep("httpRequest", step, options);
3412
- };
3413
3503
  proto.hubspotCreateCompany = function(step, options) {
3414
3504
  return this.executeStep("hubspotCreateCompany", step, options);
3415
3505
  };
@@ -3732,8 +3822,7 @@ function resolveToken(provided, config) {
3732
3822
  if (provided) return { token: provided, authType: "apiKey" };
3733
3823
  if (process.env.MINDSTUDIO_API_KEY)
3734
3824
  return { token: process.env.MINDSTUDIO_API_KEY, authType: "apiKey" };
3735
- if (config?.apiKey)
3736
- return { token: config.apiKey, authType: "apiKey" };
3825
+ if (config?.apiKey) return { token: config.apiKey, authType: "apiKey" };
3737
3826
  throw new MindStudioError(
3738
3827
  "No API key provided. Run `mindstudio login`, pass `apiKey` to the constructor, or set the MINDSTUDIO_API_KEY environment variable.",
3739
3828
  "missing_api_key",
@@ -3808,7 +3897,11 @@ var init_client = __esm({
3808
3897
  get _currentHttpConfig() {
3809
3898
  const rctx = getRequestContext();
3810
3899
  if (rctx?.remoteHostname) {
3811
- return { ...this._httpConfig, baseUrl: rctx.remoteHostname, token: this._token };
3900
+ return {
3901
+ ...this._httpConfig,
3902
+ baseUrl: rctx.remoteHostname,
3903
+ token: this._token
3904
+ };
3812
3905
  }
3813
3906
  return this._httpConfig;
3814
3907
  }
@@ -3824,7 +3917,11 @@ var init_client = __esm({
3824
3917
  _getContext() {
3825
3918
  const rctx = getRequestContext();
3826
3919
  if (rctx?.auth && rctx?.databases) {
3827
- return { auth: rctx.auth, databases: rctx.databases, authConfig: rctx.authConfig };
3920
+ return {
3921
+ auth: rctx.auth,
3922
+ databases: rctx.databases,
3923
+ authConfig: rctx.authConfig
3924
+ };
3828
3925
  }
3829
3926
  return this._context;
3830
3927
  }
@@ -3887,7 +3984,8 @@ var init_client = __esm({
3887
3984
  const res = await fetch(pollUrl, {
3888
3985
  headers: { "User-Agent": "@mindstudio-ai/agent" }
3889
3986
  });
3890
- if (res.status === 502 || res.status === 503 || res.status === 504) continue;
3987
+ if (res.status === 502 || res.status === 503 || res.status === 504)
3988
+ continue;
3891
3989
  if (res.status === 404) {
3892
3990
  throw new MindStudioError(
3893
3991
  `[${stepType}] Execution token expired.`,
@@ -3984,7 +4082,9 @@ var init_client = __esm({
3984
4082
  const errMsg = (typeof body2.error === "string" ? body2.error : void 0) ?? (typeof body2.message === "string" ? body2.message : void 0) ?? (typeof body2.details === "string" ? body2.details : void 0);
3985
4083
  if (errMsg) message = errMsg;
3986
4084
  else if (body2.error || body2.message || body2.details) {
3987
- message = JSON.stringify(body2.error ?? body2.message ?? body2.details);
4085
+ message = JSON.stringify(
4086
+ body2.error ?? body2.message ?? body2.details
4087
+ );
3988
4088
  }
3989
4089
  if (body2.code) code = body2.code;
3990
4090
  } catch {
@@ -3993,7 +4093,12 @@ var init_client = __esm({
3993
4093
  }
3994
4094
  } catch {
3995
4095
  }
3996
- throw new MindStudioError(`[${stepType}] ${message}`, code, res.status, details);
4096
+ throw new MindStudioError(
4097
+ `[${stepType}] ${message}`,
4098
+ code,
4099
+ res.status,
4100
+ details
4101
+ );
3997
4102
  }
3998
4103
  const headers = res.headers;
3999
4104
  try {
@@ -4125,7 +4230,10 @@ var init_client = __esm({
4125
4230
  async executeStepBatch(steps, options) {
4126
4231
  const threadId = options?.threadId ?? (this._reuseThreadId && !getRequestContext() ? this._threadId : void 0);
4127
4232
  const { data: asyncData } = await request(this._currentHttpConfig, "POST", "/steps/execute-batch-async", {
4128
- steps: steps.map((s) => ({ ...s, stepType: resolveStepType2(s.stepType) })),
4233
+ steps: steps.map((s) => ({
4234
+ ...s,
4235
+ stepType: resolveStepType2(s.stepType)
4236
+ })),
4129
4237
  ...options?.appId != null && { appId: options.appId },
4130
4238
  ...threadId != null && { threadId }
4131
4239
  });
@@ -4137,7 +4245,8 @@ var init_client = __esm({
4137
4245
  const res = await fetch(pollUrl, {
4138
4246
  headers: { "User-Agent": "@mindstudio-ai/agent" }
4139
4247
  });
4140
- if (res.status === 502 || res.status === 503 || res.status === 504) continue;
4248
+ if (res.status === 502 || res.status === 503 || res.status === 504)
4249
+ continue;
4141
4250
  if (res.status === 404) {
4142
4251
  throw new MindStudioError(
4143
4252
  "Batch poll token not found or expired.",
@@ -4301,7 +4410,8 @@ var init_client = __esm({
4301
4410
  const res = await fetch(pollUrl, {
4302
4411
  headers: { "User-Agent": "@mindstudio-ai/agent" }
4303
4412
  });
4304
- if (res.status === 502 || res.status === 503 || res.status === 504) continue;
4413
+ if (res.status === 502 || res.status === 503 || res.status === 504)
4414
+ continue;
4305
4415
  if (res.status === 404) {
4306
4416
  throw new MindStudioError(
4307
4417
  "Poll token not found or expired.",
@@ -4417,6 +4527,15 @@ var init_client = __esm({
4417
4527
  );
4418
4528
  return data;
4419
4529
  }
4530
+ /** List packaged workflows available to the organization. */
4531
+ async listPackagedWorkflows() {
4532
+ const { data } = await request(
4533
+ this._currentHttpConfig,
4534
+ "GET",
4535
+ "/helpers/packaged-workflows"
4536
+ );
4537
+ return data;
4538
+ }
4420
4539
  // -------------------------------------------------------------------------
4421
4540
  // Helper methods — cost estimation
4422
4541
  // -------------------------------------------------------------------------
@@ -4647,7 +4766,9 @@ var init_client = __esm({
4647
4766
  const errMsg = (typeof body.error === "string" ? body.error : void 0) ?? (typeof body.message === "string" ? body.message : void 0) ?? (typeof body.details === "string" ? body.details : void 0);
4648
4767
  if (errMsg) message = errMsg;
4649
4768
  else if (body.error || body.message || body.details) {
4650
- message = JSON.stringify(body.error ?? body.message ?? body.details);
4769
+ message = JSON.stringify(
4770
+ body.error ?? body.message ?? body.details
4771
+ );
4651
4772
  }
4652
4773
  if (body.code) code = body.code;
4653
4774
  } catch {
@@ -4655,11 +4776,7 @@ var init_client = __esm({
4655
4776
  }
4656
4777
  } catch {
4657
4778
  }
4658
- throw new MindStudioError(
4659
- `[db] ${message}`,
4660
- code,
4661
- res.status
4662
- );
4779
+ throw new MindStudioError(`[db] ${message}`, code, res.status);
4663
4780
  }
4664
4781
  const data = await res.json();
4665
4782
  return data.results;
@@ -4754,6 +4871,7 @@ var init_client = __esm({
4754
4871
  minutes: (n) => n * 6e4,
4755
4872
  ago: (ms) => Date.now() - ms,
4756
4873
  fromNow: (ms) => Date.now() + ms,
4874
+ userRef: (id) => id.startsWith("@@user@@") ? id.slice("@@user@@".length) : id,
4757
4875
  // Batch needs context — hydrate first, then delegate to real db
4758
4876
  batch: ((...queries) => {
4759
4877
  return (async () => {
@@ -4868,9 +4986,14 @@ var init_client = __esm({
4868
4986
  }
4869
4987
  /** Update the profile picture of the authenticated user/agent. */
4870
4988
  async changeProfilePicture(url) {
4871
- await request(this._currentHttpConfig, "POST", "/account/change-profile-picture", {
4872
- url
4873
- });
4989
+ await request(
4990
+ this._currentHttpConfig,
4991
+ "POST",
4992
+ "/account/change-profile-picture",
4993
+ {
4994
+ url
4995
+ }
4996
+ );
4874
4997
  }
4875
4998
  /**
4876
4999
  * Upload a file to the MindStudio CDN.
@@ -5163,8 +5286,12 @@ function buildReferenceDocs(data) {
5163
5286
 
5164
5287
  Database queries (Query, Mutation) support both \`await\` and \`.then()/.catch()\` \u2014 use standard try/catch with await or chain \`.catch()\` directly.
5165
5288
 
5289
+ Filter predicates and bindings: Filters compile to SQL when the predicate uses field comparisons, &&/||, .includes(), null checks, and boolean fields with literal values. Predicates that reference outer-scope variables (e.g. \`o => o.companyId === input.companyId\`) cannot be compiled \u2014 JavaScript closures aren't inspectable from outside the function \u2014 so they fall back to JS and scan the whole table. For filters on tables that may grow, ALWAYS use the explicit-bindings form so the filter compiles to SQL: \`Investments.filter((i, $) => i.companyId === $.companyId, { companyId: input.companyId })\`. The second predicate parameter (any name \u2014 \`$\`, \`vars\`, \`b\`) signals that bindings are in play; the third argument provides the values. Supported on filter, findOne, count, some, every, removeAll. Bound array \u2192 \`$.ids.includes(o.field)\` compiles to IN; bound string \u2192 \`o.field.includes($.text)\` compiles to LIKE. Missing keys fall back to JS \u2014 the SDK does not substitute NULL. ALWAYS include an inline code comment when you write the bindings form so downstream coding agents don't "simplify" the bindings argument away, e.g. \`{ companyId: input.companyId } // bindings: lifts closure var so filter compiles to SQL\`.
5290
+
5166
5291
  Auth + DB identity: When an app has auth enabled, the authenticated user IS a row in the app's users table. \`auth.userId\` is the row's \`id\` \u2014 do NOT add a separate \`userId\` column. Access user data with \`Users.get(auth.userId)\`. The platform creates the user row on first login and manages the \`email\`, \`phone\`, and \`roles\` columns automatically. IMPORTANT: The platform only populates the mapped auth columns (email, phone, roles) when creating the row \u2014 all other columns will be null until the developer's code sets them. Non-auth columns on the user table should be typed as optional (e.g. \`username?: string\`) and null-checked before use.
5167
5292
 
5293
+ User-typed columns: The \`User\` type is just \`string\` at runtime. Write plain UUIDs to user-typed columns \u2014 do NOT add a \`@@user@@\` prefix manually and do NOT use \`as any\`. The SDK adds and strips the prefix automatically on read/write. If TypeScript complains about assigning a plain string to a \`User\` field, use \`db.userRef(id)\` to type-cast cleanly. Reads always return bare UUIDs; use \`resolveUser(id)\` when you need display info (name, email, avatar). Never do your own prefix handling \u2014 it will fight the SDK and cause double-prefixing or orphan refs. The \`@@user@@\` prefix is only visible in storage if you bypass the SDK with raw SQL; app code should never see or produce it.
5294
+
5168
5295
  Task agents: For multi-step tasks requiring autonomous tool use, use \`runTask()\`. Provide a prompt, input, SDK action names as tools (with optional default overrides), a \`structuredOutputExample\`, and a model. The platform runs a tool-use loop and returns structured output. Tools can include any SDK action \u2014 e.g. \`['searchGoogle', 'fetchUrl', { method: 'generateImage', defaults: { imageModelOverride: { model: 'seedream-4.5' } } }]\`. Supports SSE streaming via \`onEvent\` callback.
5169
5296
 
5170
5297
  Table options: \`db.defineTable<T>(name, { unique, defaults })\`.
@@ -5241,6 +5368,7 @@ var init_instructions = __esm({
5241
5368
  - **Building custom HTTP integrations when a connector exists** \u2014 if they're asking how to call the Slack API, Airtable API, HubSpot API, etc. via \`httpRequest\`, the answer is \`runFromConnectorRegistry\` with an existing OAuth connector. 850+ connector actions exist for this. If the user's has not configured the specific connectio in MindStudio yet, that should be step one. Direct them to https://app.mindstudio.ai/services/integrations
5242
5369
  - **Missing MindStudioError handling** \u2014 the SDK has structured errors with \`code\`, \`status\`, \`details\`. Catching generic \`Error\` loses actionable information. Always include \`MindStudioError\` handling in code examples.
5243
5370
  - **One-at-a-time db writes when batch exists** \u2014 N sequential \`update()\` or \`push()\` calls should be a single \`db.batch()\` call. One round trip instead of N.
5371
+ - **Closure variables in filter predicates** \u2014 \`Table.filter(o => o.x === input.x)\` cannot compile to SQL (JS closures aren't introspectable from outside the function), so it scans every row in the table in JS. For filters comparing to outer-scope values, use the explicit bindings form: \`Table.filter((o, $) => o.x === $.x, { x: input.x })\`. Recommend this whenever the predicate references a value from the enclosing function and the table might have more than a few hundred rows.
5244
5372
  - **Hardcoded model IDs without context** \u2014 model IDs can change. When writing code with a specific model, include a comment noting which model it is and why it was chosen, so the caller can swap it later.
5245
5373
  - **Adding a userId column to the auth users table** \u2014 \`auth.userId\` IS the row's \`id\` in the users table. The platform creates the row on login. Don't add a separate \`userId\` column \u2014 just use \`Users.get(auth.userId)\` to access the user's row.
5246
5374
  - Do not recommend using thread persistence. This is an advanced feature - if someone needs this they will discover it on their own.
@@ -5277,7 +5405,7 @@ var init_instructions = __esm({
5277
5405
  - **Always recommend latest-generation models**: For Anthropic, this is Claude 4 family, GPT-5 for OpenAI, Gemini 3 for Google, etc. MindStudio supports a ton of different models, including legacy models - but they are there for niche uses or backward compatibility/existing user requirements - they should NOT be used for new projects.
5278
5406
  - Use the names of model input objects to infer **technical capabilities** \u2014 e.g., a model with a source image input supports image editing, a video model with start/end frame supports looping. Do not look at model tags or descriptions to determine capabilities.
5279
5407
  - **Do NOT use config presets/enums to judge model quality or recommend models.** A model having style presets (e.g. "oil painting", "watercolor") does NOT make it better at those styles than models without presets. The recommended models (Seedream, Nano Banana Pro, etc.) are far more capable and achieve any style through prompting alone. Presets are a crutch for weaker models \u2014 never recommend a model just because it has an enum that matches the user's request.
5280
- - **Never set maxResponseTokens to a low value.** Many models use extended thinking/reasoning that counts against the token limit. Setting maxResponseTokens to 256 or 512 will cause truncated or failed responses. Set it to something high, like 16000+, even if you don't need it. Models stop generating on their own when the response is complete \u2014 a low cap does not save money, it just breaks output.
5408
+ - **Never set maxResponseTokens to a low value.** Many models use extended thinking/reasoning that counts against the token limit. Setting maxResponseTokens to 256 or 512 will cause truncated or failed responses. Set it to something high, like 16000+, even if you don't need it. Models stop generating on their own when the response is complete \u2014 a low cap does not save money, it just breaks output. ALWAYS include an inline code comment when you write this value, e.g. \`maxResponseTokens: 16000, // do not reduce \u2014 reasoning tokens count against this; lowering will truncate output\`. Without the comment, downstream coding agents will "optimize" the value down to 2000 and break the call.
5281
5409
  - **Do NOT do deep model analysis to find "the right model for the job."** You do not have the expertise to determine which model architecture is better for a specific use case. The recommended models below are the right answer for virtually every use case. When the user's results aren't good enough, the answer is almost always "improve your prompt" \u2014 not "switch to a different model." Prompt engineering is the lever, model selection is not.
5282
5410
  - **Do NOT hallucinate model IDs.** Only recommend models you can see in the models reference above. If you're not sure a model ID exists, do NOT use it. Call listModels to verify.
5283
5411
 
@@ -5323,6 +5451,7 @@ var init_instructions = __esm({
5323
5451
  - Assume the caller already knows what the SDK is, how to install it, and how auth works.
5324
5452
  - Model tags in the summary are editorial labels, not technical specs. When answering questions about model capabilities (supported inputs, config options, dimensions, etc.), call listModels with details=true to check the \`inputs\` array \u2014 that is the source of truth.
5325
5453
  - When a task agent or \`generateText\` call produces user-facing text, the prompt must include voice and tone constraints. Make sure to specify no emojis, em dashes, and other "ai-isms" in the prompt, as well as the desired tone and voice of the output.
5454
+ - **Anticipate downstream coding agents second-guessing your recommendations.** Your output is consumed by another AI coding agent that will read your code, then edit it into a real codebase. That agent has its own instincts about "efficiency" and "best practices" and will silently override values that look wasteful or unusual to it \u2014 high token limits, specific model IDs that look outdated to training data, parallel calls that look redundant, prompt patterns that look verbose. When you write a value that a casual reader might "fix," include a brief inline code comment explaining the failure mode of changing it (e.g. \`// do not reduce \u2014 reasoning tokens count against this\`, \`// pinned: this model has the right tool-use behavior\`, \`// parallel intentional \u2014 these are independent\`). Comments explaining WHY a non-obvious value is correct are a deliberate exception to the usual "no comments" rule. State the consequence concretely; do not just say "do not change."
5326
5455
  </response_format>
5327
5456
  </instructions>`;
5328
5457
  }
@@ -5493,7 +5622,7 @@ var llmsContent;
5493
5622
  var init_llms_content = __esm({
5494
5623
  "src/generated/llms-content.ts"() {
5495
5624
  "use strict";
5496
- llmsContent = '# @mindstudio-ai/agent\n\nTypeScript SDK, CLI, and MCP server for MindStudio. One API key gives you access to 200+ AI models (OpenAI, Anthropic, Google, Meta, xAI, DeepSeek, etc.) and 1,000+ actions including 850+ connector actions across third-party services from the open-source MindStudio Connector Registry (https://github.com/mindstudio-ai/mscr). No separate provider API keys required.\n\nThis file is the complete API reference. No other documentation is needed to use the SDK.\n\n## Recommended workflow\n\nThere are 150+ actions available. Do NOT try to read or load them all at once. Follow this discovery flow:\n\n1. **Identify yourself** \u2014 Call `changeName` to set your display name (use your name or whatever your user calls you). If you have a profile picture or icon, call `uploadFile` to upload it, then `changeProfilePicture` with the returned URL. This helps users identify your requests in their logs.\n2. **Ask** \u2014 Use `mindstudio ask "your question"` (CLI) or the `ask` MCP tool for SDK guidance. It knows every action, model, and connector and returns working TypeScript code with real model IDs and config options. Examples: `mindstudio ask "generate an image with FLUX"`, `mindstudio ask "what models support vision?"`, `mindstudio ask "how do I send a Slack message?"`.\n3. **Browse** \u2014 For manual discovery, call `listActions` (MCP tool) or `mindstudio list-actions --summary` (CLI) to get a compact `{ action: description }` map of everything available (~3k tokens). Call `mindstudio info <action>` (CLI) for parameter details.\n4. **Call it** \u2014 Invoke the action with the required parameters. All actions share the same calling convention (see below).\n\nFor specific use cases:\n\n- **OAuth third-party integrations** (Slack, Google, HubSpot, etc.): These are optional OAuth connectors from the MindStudio Connector Registry \u2014 for most tasks, use actions directly instead. If you need a third-party integration: call `listConnectors()` to browse services \u2192 `getConnectorAction(serviceId, actionId)` for input fields \u2192 execute via `runFromConnectorRegistry`. Requires an OAuth connection set up in MindStudio first \u2014 call `listConnections()` to check available connections.\n- **Pre-built agents**: Call `listAgents()` to see what\'s available \u2192 `runAgent({ appId })` to execute one. **Important:** Not all agents are configured for API use. Do not try to run an agent just because it appears in the list \u2014 only run agents the user specifically asks you to run.\n- **Model selection**: Call `listModelsSummary()` or `listModelsSummaryByType("llm_chat")` to browse models, then pass the model ID as `modelOverride.model` to actions like `generateText`. Use the summary endpoints (not `listModels`) to keep token usage low.\n- **Cost estimation**: AI-powered actions (text generation, image generation, video, audio, etc.) cost money. Call `estimateStepCost(stepType, stepInput)` before running these and confirm with the user before proceeding \u2014 unless they\'ve explicitly given permission to go ahead. Non-AI actions (data lookups, OAuth connectors, etc.) are generally free.\n- **Task agents**: For multi-step tasks that need autonomous tool use (research, content creation, data enrichment), use `runTask()`. Provide a prompt, input, SDK action names as tools, and a structured output example. The platform runs a tool-use loop \u2014 calling the model, executing actions, feeding results back \u2014 until structured output is produced. Example: `runTask({ prompt: "Find info about this restaurant", input: { name: "Tartine" }, tools: ["searchGoogle", "fetchUrl"], structuredOutputExample: \'{"name":"...","url":"..."}\', model: "claude-4-6-sonnet" })`.\n\n## Install\n\nStandalone binary (CLI/MCP, no dependencies):\n```bash\ncurl -fsSL https://msagent.ai/install.sh | bash\n```\n\nnpm (SDK + CLI):\n```bash\nnpm install @mindstudio-ai/agent\n```\n\nRequires Node.js >= 18.\n\n## CLI\n\nThe package includes a CLI for executing steps from the command line or scripts:\n\n```bash\n# Execute with named flags (kebab-case)\nmindstudio generate-image --prompt "A mountain landscape"\n\n# Execute with JSON input (JSON5-tolerant)\nmindstudio generate-image \'{prompt: "A mountain landscape"}\'\n\n# Extract a single output field\nmindstudio generate-image --prompt "A sunset" --output-key imageUrl\n\n# List all methods (compact JSON \u2014 best for LLM discovery)\nmindstudio list --summary\n\n# List all methods (human-readable table)\nmindstudio list\n\n# Show method details (params, types, output)\nmindstudio info generate-image\n\n# Run via npx without installing\nnpx @mindstudio-ai/agent generate-text --message "Hello"\n```\n\nAuth: run `mindstudio login`, set `MINDSTUDIO_API_KEY` env var, or pass `--api-key <key>`.\nMethod names are kebab-case on the CLI (camelCase also accepted). Flags are kebab-case (`--video-url` for `videoUrl`).\nUse `--output-key <key>` to extract a single field, `--no-meta` to strip $-prefixed metadata.\n\n### Authentication\n\n```bash\n# Interactive login (opens browser, saves key to ~/.mindstudio/config.json)\nmindstudio login\n\n# Check current auth status\nmindstudio whoami\n\n# Clear stored credentials\nmindstudio logout\n```\n\nAuth resolution order: `--api-key` flag > `MINDSTUDIO_API_KEY` env > `~/.mindstudio/config.json` > `CALLBACK_TOKEN` env.\n\n## MCP server\n\nThe package includes an MCP server exposing all methods as tools. Start by calling the `listSteps` tool to discover available methods.\n\n```bash\nmindstudio mcp\n```\n\nMCP client config (standalone binary \u2014 recommended):\n```json\n{\n "mcpServers": {\n "mindstudio": {\n "command": "mindstudio",\n "args": ["mcp"],\n "env": { "MINDSTUDIO_API_KEY": "your-api-key" }\n }\n }\n}\n```\n\n## Setup\n\nInside MindStudio apps (auth is automatic):\n```typescript\nimport { mindstudio, db, auth, Roles, stream } from \'@mindstudio-ai/agent\';\n\nconst { imageUrl } = await mindstudio.generateImage({ prompt: \'a sunset\' });\n```\n\nExternal usage (API key required):\n```typescript\nimport { MindStudioAgent } from \'@mindstudio-ai/agent\';\nconst agent = new MindStudioAgent({ apiKey: \'your-key\' });\n```\n\nYour MindStudio API key authenticates all requests. MindStudio routes to the correct AI provider (OpenAI, Google, Anthropic, etc.) server-side \u2014 you do NOT need separate provider API keys.\n\n## Models\n\nDirect access to 200+ AI models from every major provider \u2014 all through a single API key, billed at cost with no markups.\n\nUse `listModels()` or `listModelsByType()` for full model details, or `listModelsSummary()` / `listModelsSummaryByType()` for a lightweight list (id, name, type, tags) suitable for LLM context windows. Pass a model ID to `modelOverride.model` in methods like `generateText` to select a specific model:\n\n```typescript\nconst { models } = await mindstudio.listModelsByType(\'llm_chat\');\nconst model = models.find(m => m.name.includes("Gemini"));\n\nconst { content } = await mindstudio.generateText({\n message: \'Hello\',\n modelOverride: {\n model: model.id,\n temperature: 0.7,\n maxResponseTokens: 16000,\n },\n});\n```\n\n## Calling convention\n\nEvery method has the signature:\n```typescript\nmindstudio.methodName(input: InputType, options?: { appId?, threadId?, onLog? }): Promise<OutputType & StepExecutionMeta>\n```\n\nThe first argument is the step-specific input object. The optional second argument controls thread/app context and debug logging.\n\n**Debug logging**: Pass `onLog` in the options to get real-time debug logs during execution (SSE streaming). Works on ALL step methods:\n```typescript\nawait mindstudio.generateImage({ prompt: \'...\' }, {\n onLog: (event) => console.log(`[${event.tag}] ${event.value}`),\n});\n// event: { value: string, tag: string, ts: number }\n```\n\n**`stream()` \u2014 push real-time updates to the frontend:**\n```typescript\nimport { mindstudio, stream } from \'@mindstudio-ai/agent\';\n\n// String \u2192 \'token\' event (frontend receives via onToken, accumulated text)\nawait stream(\'Processing...\');\n\n// Object \u2192 \'data\' event (frontend receives via onStreamData, NOT onToken)\nawait stream({ status: \'generating\', progress: 50 });\n```\nThese are different event types \u2014 do not mix them up. `stream()` is silently ignored when no SSE connection is active (safe to include unconditionally). For long-running steps, combine `onLog` with `stream()` to push progress to the frontend.\n\n**Results are returned flat** \u2014 output fields are spread at the top level alongside metadata:\n\n```typescript\nconst { content } = await mindstudio.generateText({ message: \'Hello\' });\n\n// Full result shape for any method:\nconst result = await mindstudio.generateText({ message: `Hello` });\nresult.content; // step-specific output field\nresult.$appId; // string \u2014 app ID for this execution\nresult.$threadId; // string \u2014 thread ID for this execution\nresult.$rateLimitRemaining; // number | undefined \u2014 API calls remaining in rate limit window\nresult.$billingCost; // number | undefined \u2014 cost in credits for this call\nresult.$billingEvents; // object[] | undefined \u2014 itemized billing events\n```\n\n## Thread persistence\n\nPass `$appId`/`$threadId` from a previous result to maintain conversation state, variable state, or other context across calls:\n\n```typescript\nconst r1 = await mindstudio.generateText({ message: \'My name is Alice\' });\nconst r2 = await mindstudio.generateText(\n { message: \'What is my name?\' },\n { threadId: r1.$threadId, appId: r1.$appId },\n);\n// r2.content => "Your name is Alice"\n```\n\n## Error handling\n\nAll errors throw `MindStudioError`:\n```typescript\nimport { MindStudioError } from \'@mindstudio-ai/agent\';\n\ntry {\n await mindstudio.generateImage({ prompt: \'...\' });\n} catch (err) {\n if (err instanceof MindStudioError) {\n err.message; // Human-readable error message\n err.code; // Machine-readable code: "invalid_step_config", "api_error", "call_cap_exceeded", "output_fetch_error"\n err.status; // HTTP status code (400, 401, 429, etc.)\n err.details; // Raw error body from the API\n }\n}\n```\n\n429 rate limit errors are retried automatically (configurable via `maxRetries`).\n\n## Low-level access\n\nFor action types not covered by generated methods:\n```typescript\nconst result = await mindstudio.executeStep(\'stepType\', { ...params });\n```\n\n## Batch execution\n\nExecute multiple steps in parallel in a single request. Maximum 50 steps per batch.\nIndividual step failures do not affect other steps \u2014 partial success is possible.\n\n```typescript\nconst result = await mindstudio.executeStepBatch([\n { stepType: \'generateImage\', step: { prompt: \'a sunset\' } },\n { stepType: \'textToSpeech\', step: { text: \'hello world\' } },\n], { appId?, threadId? });\n\n// Result:\nresult.results; // BatchStepResult[] \u2014 same order as input\nresult.results[0].stepType; // string\nresult.results[0].output; // object | undefined (step output on success)\nresult.results[0].error; // string | undefined (error message on failure)\nresult.results[0].billingCost; // number | undefined (cost on success)\nresult.totalBillingCost; // number | undefined\nresult.appId; // string\nresult.threadId; // string\n```\n\nCLI:\n```bash\nmindstudio batch \'[{"stepType":"generateImage","step":{"prompt":"a cat"}}]\'\ncat steps.json | mindstudio batch\n```\n\n## Methods\n\nAll methods below are called on a `MindStudioAgent` instance (`agent.methodName(...)`).\nInput shows the first argument object. Output shows the fields available on the returned result.\n\n### General\n\n#### addSubtitlesToVideo\nAutomatically add subtitles to a video\n- Can control style of text and animation\n- Input: `{ videoUrl: string, language: string, fontName: string, fontSize: number, fontWeight: "normal" | "bold" | "black", fontColor: "white" | "black" | "red" | "green" | "blue" | "yellow" | "orange" | "purple" | "pink" | "brown" | "gray" | "cyan" | "magenta", highlightColor: "white" | "black" | "red" | "green" | "blue" | "yellow" | "orange" | "purple" | "pink" | "brown" | "gray" | "cyan" | "magenta", strokeWidth: number, strokeColor: "black" | "white" | "red" | "green" | "blue" | "yellow" | "orange" | "purple" | "pink" | "brown" | "gray" | "cyan" | "magenta", backgroundColor: "black" | "white" | "red" | "green" | "blue" | "yellow" | "orange" | "purple" | "pink" | "brown" | "gray" | "cyan" | "magenta" | "none", backgroundOpacity: number, position: "top" | "center" | "bottom", yOffset: number, wordsPerSubtitle: number, enableAnimation: boolean, intermediateAsset?: boolean }`\n- Output: `{ videoUrl: string }`\n\n#### analyzeImage\nAnalyze an image using a vision model based on a text prompt.\n- Uses the configured vision model to generate a text analysis of the image.\n- The prompt should describe what to look for or extract from the image.\n- Input: `{ prompt: string, imageUrl: string, visionModelOverride?: { model: string, config?: object } | { model: string, temperature: number, maxResponseTokens: number, ignorePreamble?: boolean, userMessagePreprocessor?: { dataSource?: string, messageTemplate?: string, maxResults?: number, enabled?: boolean, shouldInherit?: boolean }, preamble?: string, multiModelEnabled?: boolean, editResponseEnabled?: boolean, config?: object } }`\n- Output: `{ analysis: string }`\n\n#### analyzeVideo\nAnalyze a video using a video analysis model based on a text prompt.\n- Uses the configured video analysis model to generate a text analysis of the video.\n- The prompt should describe what to look for or extract from the video.\n- Input: `{ prompt: string, videoUrl: string, videoAnalysisModelOverride?: { model: string, config?: object } | { model: string, temperature: number, maxResponseTokens: number, ignorePreamble?: boolean, userMessagePreprocessor?: { dataSource?: string, messageTemplate?: string, maxResults?: number, enabled?: boolean, shouldInherit?: boolean }, preamble?: string, multiModelEnabled?: boolean, editResponseEnabled?: boolean, config?: object } }`\n- Output: `{ analysis: string }`\n\n#### captureThumbnail\nCapture a thumbnail from a video at a specified timestamp\n- Input: `{ videoUrl: string, at: number | string }`\n- Output: `{ thumbnailUrl: string }`\n\n#### checkAppRole\nCheck whether the current user has a specific app role and branch accordingly.\n- Checks if the current user has been assigned a specific role in this app.\n- If the user has the role, transitions to the "has role" path.\n- If the user does not have the role, transitions to the "no role" path, or errors if no path is configured.\n- Role names are defined by the app creator and assigned to users via the app roles system.\n- The roleName field supports {{variables}} for dynamic role checks.\n- Input: `{ roleName: string, hasRoleStepId?: string, hasRoleWorkflowId?: string, noRoleStepId?: string, noRoleWorkflowId?: string }`\n- Output: `{ hasRole: boolean, userRoles: string[] }`\n\n#### convertPdfToImages\nConvert each page of a PDF document into a PNG image.\n- Each page is converted to a separate PNG and re-hosted on the CDN.\n- Returns an array of image URLs, one per page.\n- Input: `{ pdfUrl: string }`\n- Output: `{ imageUrls: string[] }`\n\n#### createDataSource\nCreate a new empty vector data source for the current app.\n- Creates a new data source (vector database) associated with the current app version.\n- The data source is created empty \u2014 use the "Upload Data Source Document" block to add documents.\n- Returns the new data source ID which can be used in subsequent blocks.\n- Input: `{ name: string }`\n- Output: `unknown`\n\n#### createGmailDraft\nCreate a draft email in the connected Gmail account.\n- Requires a Google OAuth connection with Gmail compose scope.\n- The draft appears in the user\'s Gmail Drafts folder but is not sent.\n- messageType controls the body format: "plain" for plain text, "html" for raw HTML, "markdown" for auto-converted markdown.\n- Input: `{ to: string, subject: string, message: string, connectionId?: string, messageType: "plain" | "html" | "markdown" }`\n- Output: `{ draftId: string }`\n\n#### deleteDataSource\nDelete a vector data source from the current app.\n- Soft-deletes a data source (vector database) by marking it as deleted.\n- The Milvus partition is cleaned up asynchronously by a background cron job.\n- The data source must belong to the current app version.\n- Input: `{ dataSourceId: string }`\n- Output: `unknown`\n\n#### deleteDataSourceDocument\nDelete a single document from a data source.\n- Soft-deletes a document by marking it as deleted.\n- Requires both the data source ID and document ID.\n- After deletion, reloads vectors into Milvus so the data source reflects the change immediately.\n- Input: `{ dataSourceId: string, documentId: string }`\n- Output: `unknown`\n\n#### detectChanges\nDetect changes between runs by comparing current input against previously stored state. Routes execution based on whether a change occurred.\n- Persists state across runs using a global variable keyed to the step ID.\n- Two modes: "comparison" (default) uses strict string inequality; "ai" uses an LLM to determine if a meaningful change occurred.\n- First run always treats the value as "changed" since there is no previous state.\n- Each mode supports transitions to different steps/workflows for the "changed" and "unchanged" paths.\n- AI mode bills normally for the LLM call.\n- Input: `{ mode: "ai" | "comparison", input: string, prompt?: string, modelOverride?: { model: string, temperature: number, maxResponseTokens: number, ignorePreamble?: boolean, userMessagePreprocessor?: { dataSource?: string, messageTemplate?: string, maxResults?: number, enabled?: boolean, shouldInherit?: boolean }, preamble?: string, multiModelEnabled?: boolean, editResponseEnabled?: boolean, config?: object }, previousValueVariable?: string, changedStepId?: string, changedWorkflowId?: string, unchangedStepId?: string, unchangedWorkflowId?: string }`\n- Output: `{ hasChanged: boolean, currentValue: string, previousValue: string, isFirstRun: boolean }`\n\n#### detectPII\nScan text for personally identifiable information using Microsoft Presidio.\n- In workflow mode, transitions to detectedStepId if PII is found, notDetectedStepId otherwise.\n- In direct execution, returns the detection results without transitioning.\n- If entities is empty, returns immediately with no detections.\n- Input: `{ input: string, language: string, entities: string[], detectedStepId?: string, notDetectedStepId?: string, outputLogVariable?: string | null }`\n- Output: `{ detected: boolean, detections: { entity_type: string, start: number, end: number, score: number }[] }`\n\n#### discordEditMessage\nEdit a previously sent Discord channel message. Use with the message ID returned by Send Discord Message.\n- Only messages sent by the bot can be edited.\n- The messageId is returned by the Send Discord Message step.\n- Optionally attach a file by providing a URL to attachmentUrl. The file is downloaded and uploaded to Discord.\n- When editing with an attachment, the new attachment replaces any previous attachments on the message.\n- URLs in the text are automatically embedded by Discord (link previews for images, videos, etc.).\n- Input: `{ botToken: string, channelId: string, messageId: string, text: string, attachmentUrl?: string }`\n- Output: `unknown`\n\n#### discordSendFollowUp\nSend a follow-up message to a Discord slash command interaction.\n- Requires the applicationId and interactionToken from the Discord trigger variables.\n- Follow-up messages appear as new messages in the channel after the initial response.\n- Returns the sent message ID.\n- Interaction tokens expire after 15 minutes.\n- Optionally attach a file by providing a URL to attachmentUrl. The file is downloaded and uploaded to Discord.\n- URLs in the text are automatically embedded by Discord (link previews for images, videos, etc.).\n- Input: `{ applicationId: string, interactionToken: string, text: string, attachmentUrl?: string }`\n- Output: `{ messageId: string }`\n\n#### discordSendMessage\nSend a message to Discord \u2014 either edit the loading message or send a new channel message.\n- mode "edit" replaces the loading message (interaction response) with the final result. Uses applicationId and interactionToken from trigger variables. No bot permissions required.\n- mode "send" sends a new message to a channel. Uses botToken and channelId from trigger variables. Returns a messageId that can be used with Edit Discord Message.\n- Optionally attach a file by providing a URL to attachmentUrl. The file is downloaded and uploaded to Discord.\n- URLs in the text are automatically embedded by Discord (link previews for images, videos, etc.).\n- Interaction tokens expire after 15 minutes.\n- Input: `{ mode: "edit" | "send", text: string, applicationId?: string, interactionToken?: string, botToken?: string, channelId?: string, attachmentUrl?: string }`\n- Output: `{ messageId?: string }`\n\n#### downloadVideo\nDownload a video file\n- Works with YouTube, TikTok, etc., by using ytdlp behind the scenes\n- Can save as mp4 or mp3\n- Input: `{ videoUrl: string, format: "mp4" | "mp3" }`\n- Output: `{ videoUrl: string }`\n\n#### enhanceImageGenerationPrompt\nGenerate or enhance an image generation prompt using a language model. Optionally generates a negative prompt.\n- Rewrites the user\'s prompt with added detail about style, lighting, colors, and composition.\n- Also useful for initial generation, it doesn\'t always need to be enhancing an existing prompt\n- When includeNegativePrompt is true, a second model call generates a negative prompt.\n- Input: `{ initialPrompt: string, includeNegativePrompt: boolean, negativePromptDestinationVariableName?: string, systemPrompt: string, modelOverride?: unknown }`\n- Output: `{ prompt: string, negativePrompt?: string }`\n\n#### enhanceVideoGenerationPrompt\nGenerate or enhance a video generation prompt using a language model. Optionally generates a negative prompt.\n- Rewrites the user\'s prompt with added detail about style, camera movement, lighting, and composition.\n- Also useful for initial generation, it doesn\'t always need to be enhancing an existing prompt\n- When includeNegativePrompt is true, a second model call generates a negative prompt.\n- Input: `{ initialPrompt: string, includeNegativePrompt: boolean, negativePromptDestinationVariableName?: string, systemPrompt: string, modelOverride?: unknown }`\n- Output: `{ prompt: string, negativePrompt?: string }`\n\n#### extractAudioFromVideo\nExtract audio MP3 from a video file\n- Input: `{ videoUrl: string }`\n- Output: `{ audioUrl: string }`\n\n#### extractText\nDownload a file from a URL and extract its text content. Supports PDFs (including scanned/image-based PDFs via OCR), plain text files, and other document formats.\n- Best suited for PDFs and raw text/document files. For web pages, use the scrapeUrl step instead.\n- Handles both text-layer PDFs and image-based/scanned PDFs (e.g. Figma/Canva exports, scanned documents). Image-based PDFs are processed with OCR automatically \u2014 there is no need to convert PDF pages to images first.\n- Accepts a single URL, a comma-separated list of URLs, or a JSON array of URLs.\n- Files are rehosted on the MindStudio CDN before extraction.\n- Maximum file size is 50MB per URL.\n- Input: `{ url: string | string[] }`\n- Output: `{ text: string | string[] }`\n\n#### fetchDataSourceDocument\nFetch the full extracted text contents of a document in a data source.\n- Loads a document by ID and returns its full extracted text content.\n- The document must have been successfully processed (status "done").\n- Also returns document metadata (name, summary, word count).\n- Input: `{ dataSourceId: string, documentId: string }`\n- Output: `unknown`\n\n#### fetchSlackChannelHistory\nFetch recent message history from a Slack channel.\n- The user is responsible for connecting their Slack workspace and selecting the channel\n- Input: `{ connectionId?: string, channelId: string, limit?: number, startDate?: string, endDate?: string, includeImages?: boolean, includeRawMessage?: boolean }`\n- Output: `{ messages: { from: string, content: string, timestamp?: string, images?: string[], rawMessage?: { app_id?: string, assistant_app_thread?: { first_user_thread_reply?: string, title?: string, title_blocks?: unknown[] }, attachments?: { actions?: unknown[], app_id?: string, app_unfurl_url?: string, author_icon?: string, author_id?: string, author_link?: string, author_name?: string, author_subname?: string, blocks?: unknown[], bot_id?: string, bot_team_id?: string, callback_id?: string, channel_id?: string, channel_name?: string, channel_team?: string, color?: string, fallback?: string, fields?: unknown[], file_id?: string, filename?: string, files?: unknown[], footer?: string, footer_icon?: string, from_url?: string, hide_border?: boolean, hide_color?: boolean, id?: number, image_bytes?: number, image_height?: number, image_url?: string, image_width?: number, indent?: boolean, is_app_unfurl?: boolean, is_file_attachment?: boolean, is_msg_unfurl?: boolean, is_reply_unfurl?: boolean, is_thread_root_unfurl?: boolean, list?: unknown, list_record?: unknown, list_record_id?: string, list_records?: unknown[], list_schema?: unknown[], list_view?: unknown, list_view_id?: string, message_blocks?: unknown[], metadata?: unknown, mimetype?: string, mrkdwn_in?: string[], msg_subtype?: string, original_url?: string, pretext?: string, preview?: unknown, service_icon?: string, service_name?: string, service_url?: string, size?: number, text?: string, thumb_height?: number, thumb_url?: string, thumb_width?: number, title?: string, title_link?: string, ts?: string, url?: string, video_html?: string, video_html_height?: number, video_html_width?: number, video_url?: string }[], blocks?: { accessory?: unknown, alt_text?: string, api_decoration_available?: boolean, app_collaborators?: string[], app_id?: string, author_name?: string, block_id?: string, bot_user_id?: string, button_label?: string, call?: unknown, call_id?: string, description?: unknown, developer_trace_id?: string, dispatch_action?: boolean, element?: unknown, elements?: unknown[], expand?: boolean, external_id?: string, fallback?: string, fields?: unknown[], file?: unknown, file_id?: string, function_trigger_id?: string, hint?: unknown, image_bytes?: number, image_height?: number, image_url?: string, image_width?: number, is_animated?: boolean, is_workflow_app?: boolean, label?: unknown, optional?: boolean, owning_team_id?: string, provider_icon_url?: string, provider_name?: string, sales_home_workflow_app_type?: number, share_url?: string, slack_file?: unknown, source?: string, text?: unknown, thumbnail_url?: string, title?: unknown, title_url?: string, trigger_subtype?: string, trigger_type?: string, type?: unknown, url?: string, video_url?: string, workflow_id?: string }[], bot_id?: string, bot_profile?: { app_id?: string, deleted?: boolean, icons?: unknown, id?: string, name?: string, team_id?: string, updated?: number }, client_msg_id?: string, display_as_bot?: boolean, edited?: { ts?: string, user?: string }, files?: { access?: string, alt_txt?: string, app_id?: string, app_name?: string, attachments?: unknown[], blocks?: unknown[], bot_id?: string, can_toggle_canvas_lock?: boolean, canvas_printing_enabled?: boolean, canvas_template_mode?: string, cc?: unknown[], channel_actions_count?: number, channel_actions_ts?: string, channels?: string[], comments_count?: number, converted_pdf?: string, created?: number, deanimate?: string, deanimate_gif?: string, display_as_bot?: boolean, dm_mpdm_users_with_file_access?: unknown[], duration_ms?: number, edit_link?: string, edit_timestamp?: number, editable?: boolean, editor?: string, editors?: string[], external_id?: string, external_type?: string, external_url?: string, favorites?: unknown[], file_access?: string, filetype?: string, from?: unknown[], groups?: string[], has_more?: boolean, has_more_shares?: boolean, has_rich_preview?: boolean, headers?: unknown, hls?: string, hls_embed?: string, id?: string, image_exif_rotation?: number, ims?: string[], initial_comment?: unknown, is_channel_space?: boolean, is_external?: boolean, is_public?: boolean, is_restricted_sharing_enabled?: boolean, is_starred?: boolean, last_editor?: string, last_read?: number, lines?: number, lines_more?: number, linked_channel_id?: string, list_csv_download_url?: string, list_limits?: unknown, list_metadata?: unknown, media_display_type?: string, media_progress?: unknown, mimetype?: string, mode?: string, mp4?: string, mp4_low?: string, name?: string, non_owner_editable?: boolean, num_stars?: number, org_or_workspace_access?: string, original_attachment_count?: number, original_h?: string, original_w?: string, permalink?: string, permalink_public?: string, pinned_to?: string[], pjpeg?: string, plain_text?: string, pretty_type?: string, preview?: string, preview_highlight?: string, preview_is_truncated?: boolean, preview_plain_text?: string, private_channels_with_file_access_count?: number, private_file_with_access_count?: number, public_url_shared?: boolean, quip_thread_id?: string, reactions?: unknown[], saved?: unknown, sent_to_self?: boolean, shares?: unknown, show_badge?: boolean, simplified_html?: string, size?: number, source_team?: string, subject?: string, subtype?: string, team_pref_version_history_enabled?: boolean, teams_shared_with?: unknown[], template_conversion_ts?: number, template_description?: string, template_icon?: string, template_name?: string, template_title?: string, thumb_1024?: string, thumb_1024_gif?: string, thumb_1024_h?: string, thumb_1024_w?: string, thumb_160?: string, thumb_160_gif?: string, thumb_160_h?: string, thumb_160_w?: string, thumb_360?: string, thumb_360_gif?: string, thumb_360_h?: string, thumb_360_w?: string, thumb_480?: string, thumb_480_gif?: string, thumb_480_h?: string, thumb_480_w?: string, thumb_64?: string, thumb_64_gif?: string, thumb_64_h?: string, thumb_64_w?: string, thumb_720?: string, thumb_720_gif?: string, thumb_720_h?: string, thumb_720_w?: string, thumb_80?: string, thumb_800?: string, thumb_800_gif?: string, thumb_800_h?: string, thumb_800_w?: string, thumb_80_gif?: string, thumb_80_h?: string, thumb_80_w?: string, thumb_960?: string, thumb_960_gif?: string, thumb_960_h?: string, thumb_960_w?: string, thumb_gif?: string, thumb_pdf?: string, thumb_pdf_h?: string, thumb_pdf_w?: string, thumb_tiny?: string, thumb_video?: string, thumb_video_h?: number, thumb_video_w?: number, timestamp?: number, title?: string, title_blocks?: unknown[], to?: unknown[], transcription?: unknown, update_notification?: number, updated?: number, url_private?: string, url_private_download?: string, url_static_preview?: string, user?: string, user_team?: string, username?: string, vtt?: string }[], icons?: { emoji?: string, image_36?: string, image_48?: string, image_64?: string, image_72?: string }, inviter?: string, is_locked?: boolean, latest_reply?: string, metadata?: { event_payload?: unknown, event_type?: string }, parent_user_id?: string, purpose?: string, reactions?: { count?: number, name?: string, url?: string, users?: string[] }[], reply_count?: number, reply_users?: string[], reply_users_count?: number, root?: { bot_id?: string, icons?: unknown, latest_reply?: string, parent_user_id?: string, reply_count?: number, reply_users?: string[], reply_users_count?: number, subscribed?: boolean, subtype?: string, text?: string, thread_ts?: string, ts?: string, type?: string, username?: string }, subscribed?: boolean, subtype?: string, team?: string, text?: string, thread_ts?: string, topic?: string, ts?: string, type?: string, upload?: boolean, user?: string, username?: string, x_files?: string[] } }[] }`\n\n#### generateAsset\nGenerate an HTML asset and export it as a webpage, PDF, or image\n- Agents can generate HTML documents and export as webpage, PDFs, images, or videos. They do this by using the "generatePdf" block, which defines an HTML page with variables, and then the generation process renders the page to create the output and save its URL at the specified variable.\n- The template for the HTML page is generated by a separate process, and it can only use variables that have already been defined in the workflow at the time of its execution. It has full access to handlebars to render the HTML template, including a handlebars helper to render a markdown variable string as HTML (which can be useful for creating templates that render long strings). The template can also create its own simple JavaScript to do things like format dates and strings.\n- If PDF or composited image generation are part of the workflow, assistant adds the block and leaves the "source" empty. In a separate step, assistant generates a detailed request for the developer who will write the HTML.\n- Can also auto-generate HTML from a prompt (like a generate text block to generate HTML). In these cases, create a prompt with variables in the dynamicPrompt variable describing, in detail, the document to generate\n- Can either display output directly to user (foreground mode) or save the URL of the asset to a variable (background mode)\n- Input: `{ source: string, sourceType: "html" | "markdown" | "spa" | "raw" | "dynamic" | "customInterface", outputFormat: "pdf" | "png" | "html" | "mp4" | "openGraph", pageSize: "full" | "letter" | "A4" | "custom", testData: object, options?: { pageWidthPx?: number, pageHeightPx?: number, pageOrientation?: "portrait" | "landscape", rehostMedia?: boolean, videoDurationSeconds?: number }, spaSource?: { source?: string, lastCompiledSource?: string, files?: object, paths: string[], root: string, zipUrl: string }, rawSource?: string, dynamicPrompt?: string, dynamicSourceModelOverride?: { model: string, temperature: number, maxResponseTokens: number, ignorePreamble?: boolean, userMessagePreprocessor?: { dataSource?: string, messageTemplate?: string, maxResults?: number, enabled?: boolean, shouldInherit?: boolean }, preamble?: string, multiModelEnabled?: boolean, editResponseEnabled?: boolean, config?: object }, transitionControl?: "default" | "native", shareControl?: "default" | "hidden", shareImageUrl?: string, intermediateAsset?: boolean }`\n- Output: `{ url: string }`\n\n#### generateChart\nCreate a chart image using QuickChart (Chart.js) and return the URL.\n- The data field must be a Chart.js-compatible JSON object serialized as a string.\n- Supported chart types: bar, line, pie.\n- Input: `{ chart: { chartType: "bar" | "line" | "pie", data: string, options: { width: string, height: string } } }`\n- Output: `{ chartUrl: string }`\n\n#### generateImage\nGenerate an image from a text prompt using an AI model.\n- Prompts should be descriptive but concise (roughly 3\u20136 sentences).\n- Images are automatically hosted on a CDN.\n- In foreground mode, the image is displayed to the user. In background mode, the URL is saved to a variable.\n- When generateVariants is true with numVariants > 1, multiple images are generated in parallel.\n- In direct execution, foreground mode behaves as background, and userSelect variant behavior behaves as saveAll.\n- Input: `{ prompt: string, intermediateAsset?: boolean, imageModelOverride?: { model: string, config?: object }, generateVariants?: boolean, numVariants?: number, addWatermark?: boolean }`\n- Output: `{ imageUrl: string | string[] }`\n\n#### generateLipsync\nGenerate a lip sync video from provided audio and image.\n- In foreground mode, the video is displayed to the user. In background mode, the URL is saved to a variable.\n- Input: `{ intermediateAsset?: boolean, addWatermark?: boolean, lipsyncModelOverride?: { model: string, config?: object } }`\n- Output: `unknown`\n\n#### generateMusic\nGenerate an audio file from provided instructions (text) using a music model.\n- The text field contains the instructions (prompt) for the music generation.\n- In foreground mode, the audio is displayed to the user. In background mode, the URL is saved to a variable.\n- Input: `{ text: string, intermediateAsset?: boolean, musicModelOverride?: { model: string, config?: object } }`\n- Output: `unknown`\n\n#### generateStaticVideoFromImage\nConvert a static image to an MP4\n- Can use to create slides/intertitles/slates for video composition\n- Input: `{ imageUrl: string, duration: string }`\n- Output: `{ videoUrl: string }`\n\n#### generateText\nSend a message to an AI model and return the response, or echo a system message.\n- Source "user" sends the message to an LLM and returns the model\'s response.\n- Source "system" echoes the message content directly (no AI call).\n- Mode "background" saves the result to a variable. Mode "foreground" streams it to the user (not available in direct execution).\n- Structured output (JSON/CSV) can be enforced via structuredOutputType and structuredOutputExample.\n- When executed inside a v2 app method (managed sandbox or local dev tunnel),\nLLM token output can be streamed to the frontend in real time via an SSE\nside-channel. The frontend opts in by passing { stream: true } to the method\ninvocation via @mindstudio-ai/interface. Tokens are published to Redis\npub/sub as they arrive and forwarded as SSE events on the invoke response.\nThe method code itself is unchanged \u2014 streaming is transparent to the\ndeveloper. See V2ExecutionService.ts and the invoke handler in V2Apps for\nthe server-side plumbing.\n- Input: `{ message: string, source?: "user" | "system", modelOverride?: { model: string, temperature: number, maxResponseTokens: number, ignorePreamble?: boolean, userMessagePreprocessor?: { dataSource?: string, messageTemplate?: string, maxResults?: number, enabled?: boolean, shouldInherit?: boolean }, preamble?: string, multiModelEnabled?: boolean, editResponseEnabled?: boolean, config?: object }, structuredOutputType?: "text" | "json" | "csv", structuredOutputExample?: string, chatHistoryMode?: "include" | "exclude" }`\n- Output: `{ content: string }`\n\n#### generateVideo\nGenerate a video from a text prompt using an AI model.\n- Prompts should be descriptive but concise (roughly 3\u20136 sentences).\n- Videos are automatically hosted on a CDN.\n- In foreground mode, the video is displayed to the user. In background mode, the URL is saved to a variable.\n- When generateVariants is true with numVariants > 1, multiple videos are generated in parallel.\n- In direct execution, foreground mode behaves as background, and userSelect variant behavior behaves as saveAll.\n- Input: `{ prompt: string, intermediateAsset?: boolean, videoModelOverride?: { model: string, config?: object }, generateVariants?: boolean, numVariants?: number, addWatermark?: boolean }`\n- Output: `{ videoUrl: string | string[] }`\n\n#### getGmailAttachments\nDownload attachments from a Gmail email and re-host them on CDN.\n- Requires a Google OAuth connection with Gmail readonly scope.\n- Attachments are uploaded to CDN and returned as URLs.\n- Attachments larger than 25MB are skipped.\n- Use the message ID from Search Gmail Emails, List Recent Gmail Emails, or Get Gmail Email steps.\n- Input: `{ messageId: string, connectionId?: string }`\n- Output: `unknown`\n\n#### getGmailUnreadCount\nGet the number of unread emails in the connected Gmail inbox.\n- Requires a Google OAuth connection with Gmail readonly scope.\n- Returns the unread message count for the inbox label.\n- This is a lightweight call that does not fetch any email content.\n- Input: `{ connectionId?: string }`\n- Output: `unknown`\n\n#### getMediaMetadata\nGet info about a media file\n- Input: `{ mediaUrl: string }`\n- Output: `{ metadata: string }`\n\n#### httpRequest\nMake an HTTP request to an external endpoint and return the response.\n- Supports GET, POST, PATCH, DELETE, and PUT methods.\n- Body can be raw JSON/text, URL-encoded form data, or multipart form data.\n- Input: `{ url: string, method: string, headers: object, queryParams: object, body: string, bodyItems: object, contentType: "none" | "application/json" | "application/x-www-form-urlencoded" | "multipart/form-data" | "custom", customContentType: string, testData?: object }`\n- Output: `{ ok: boolean, status: number, statusText: string, response: string }`\n\n#### imageFaceSwap\nReplace a face in an image with a face from another image using AI.\n- Requires both a target image and a face source image.\n- Output is re-hosted on the CDN as a PNG.\n- Input: `{ imageUrl: string, faceImageUrl: string, engine: string }`\n- Output: `{ imageUrl: string }`\n\n#### imageRemoveWatermark\nRemove watermarks from an image using AI.\n- Output is re-hosted on the CDN as a PNG.\n- Input: `{ imageUrl: string, engine: string, intermediateAsset?: boolean }`\n- Output: `{ imageUrl: string }`\n\n#### insertVideoClips\nInsert b-roll clips into a base video at a timecode, optionally with an xfade transition.\n- Input: `{ baseVideoUrl: string, overlayVideos: { videoUrl: string, startTimeSec: number }[], transition?: string, transitionDuration?: number, useOverlayAudio?: boolean, intermediateAsset?: boolean }`\n- Output: `{ videoUrl: string }`\n\n#### listDataSources\nList all data sources for the current app.\n- Returns metadata for every data source associated with the current app version.\n- Each entry includes the data source ID, name, description, status, and document list.\n- Input: `object`\n- Output: `unknown`\n\n#### listGmailLabels\nList all labels in the connected Gmail account. Use these label IDs or names with the Update Gmail Labels step.\n- Requires a Google OAuth connection with Gmail readonly scope.\n- Returns both system labels (INBOX, SENT, TRASH, etc.) and user-created labels.\n- Label type is "system" for built-in labels or "user" for custom labels.\n- Input: `{ connectionId?: string }`\n- Output: `unknown`\n\n#### listRecentGmailEmails\nList recent emails from the connected Gmail inbox.\n- Requires a Google OAuth connection with Gmail readonly scope.\n- Returns up to 100 emails (default 5), ordered by most recent first.\n- Functionally equivalent to Search Gmail Emails with an "in:inbox" query.\n- Input: `{ connectionId?: string, exportType: "json" | "text", limit: string }`\n- Output: `unknown`\n\n#### logic\nRoute execution to different branches based on AI evaluation, comparison operators, or workflow jumps.\n- Supports two modes: "ai" (default) uses an AI model to pick the most accurate statement; "comparison" uses operator-based checks.\n- In AI mode, the model picks the most accurate statement from the list. All possible cases must be specified.\n- In comparison mode, the context is the left operand and each case\'s condition is the right operand. First matching case wins. Use operator "default" as a fallback.\n- Requires at least two cases.\n- Each case can transition to a step in the current workflow (destinationStepId) or jump to another workflow (destinationWorkflowId).\n- Input: `{ mode?: "ai" | "comparison", context: string, cases: ({ id: string, condition: string, operator?: "eq" | "neq" | "gt" | "lt" | "gte" | "lte" | "exists" | "not_exists" | "contains" | "not_contains" | "default", destinationStepId?: string, destinationWorkflowId?: string } | string)[], modelOverride?: { model: string, temperature: number, maxResponseTokens: number, ignorePreamble?: boolean, userMessagePreprocessor?: { dataSource?: string, messageTemplate?: string, maxResults?: number, enabled?: boolean, shouldInherit?: boolean }, preamble?: string, multiModelEnabled?: boolean, editResponseEnabled?: boolean, config?: object } }`\n- Output: `{ selectedCase: number }`\n\n#### makeDotComRunScenario\nTrigger a Make.com (formerly Integromat) scenario via webhook and return the response.\n- The webhook URL must be configured in your Make.com scenario.\n- Input key-value pairs are sent as JSON in the POST body.\n- Response format depends on the Make.com scenario configuration.\n- Input: `{ webhookUrl: string, input: object }`\n- Output: `{ data: unknown }`\n\n#### mergeAudio\nMerge one or more clips into a single audio file.\n- Input: `{ mp3Urls: string[], fileMetadata?: object, albumArtUrl?: string, intermediateAsset?: boolean }`\n- Output: `{ audioUrl: string }`\n\n#### mergeVideos\nMerge one or more clips into a single video.\n- Input: `{ videoUrls: string[], transition?: string, transitionDuration?: number, intermediateAsset?: boolean }`\n- Output: `{ videoUrl: string }`\n\n#### mixAudioIntoVideo\nMix an audio track into a video\n- Input: `{ videoUrl: string, audioUrl: string, options: { keepVideoAudio?: boolean, audioGainDb?: number, videoGainDb?: number, loopAudio?: boolean }, intermediateAsset?: boolean }`\n- Output: `{ videoUrl: string }`\n\n#### muteVideo\nMute a video file\n- Input: `{ videoUrl: string, intermediateAsset?: boolean }`\n- Output: `{ videoUrl: string }`\n\n#### n8nRunNode\nTrigger an n8n workflow node via webhook and return the response.\n- The webhook URL must be configured in your n8n workflow.\n- Supports GET and POST methods with optional Basic authentication.\n- For GET requests, input values are sent as query parameters. For POST, they are sent as JSON body.\n- Input: `{ method: string, authentication: "none" | "basic" | "string", user: string, password: string, webhookUrl: string, input: object }`\n- Output: `{ data: unknown }`\n\n#### postToSlackChannel\nSend a message to a Slack channel via a connected bot.\n- The user is responsible for connecting their Slack workspace and selecting the channel\n- Supports both simple text messages and slack blocks messages\n- Text messages can use limited markdown (slack-only fomatting\u2014e.g., headers are just rendered as bold)\n- Input: `{ channelId: string, messageType: "string" | "blocks", message: string, connectionId?: string }`\n- Output: `unknown`\n\n#### postToZapier\nSend data to a Zapier Zap via webhook and return the response.\n- The webhook URL must be configured in the Zapier Zap settings\n- Input keys and values are sent as the JSON body of the POST request\n- The webhook response (JSON or plain text) is returned as the output\n- Input: `{ webhookUrl: string, input: object }`\n- Output: `{ data: unknown }`\n\n#### queryAppDatabase\nExecute a SQL query against the app managed database.\n- Executes raw SQL against a SQLite database managed by the app.\n- For SELECT queries, returns rows as JSON.\n- For INSERT/UPDATE/DELETE, returns the number of affected rows.\n- Use {{variables}} directly in your SQL. By default they are automatically extracted\nand passed as safe parameterized values (preventing SQL injection).\nExample: INSERT INTO contacts (name, comment) VALUES ({{name}}, {{comment}})\n- Full MindStudio handlebars syntax is supported, including helpers like {{json myVar}},\n{{get myVar "$.path"}}, {{global.orgName}}, etc.\n- Set parameterize to false for raw/dynamic SQL where variables are interpolated directly\ninto the query string. Use this when another step generates full or partial SQL, e.g.\na bulk INSERT with a precomputed VALUES list. The user is responsible for sanitization\nwhen parameterize is false.\n- Input: `{ databaseId: string, sql: string, parameterize?: boolean }`\n- Output: `{ rows: unknown[], changes: number }`\n\n#### queryDataSource\nSearch a vector data source (RAG) and return relevant document chunks.\n- Queries a vectorized data source and returns the most relevant chunks.\n- Useful for retrieval-augmented generation (RAG) workflows.\n- Input: `{ dataSourceId: string, query: string, maxResults: number }`\n- Output: `{ text: string, chunks: string[], query: string, citations: unknown[], latencyMs: number }`\n\n#### queryExternalDatabase\nExecute a SQL query against an external database connected to the workspace.\n- Requires a database connection configured in the workspace.\n- Supports PostgreSQL (including Supabase), MySQL, and MSSQL.\n- Results can be returned as JSON or CSV.\n- Input: `{ connectionId?: string, query: string, outputFormat: "json" | "csv" }`\n- Output: `{ data: unknown }`\n\n#### redactPII\nReplace personally identifiable information in text with placeholders using Microsoft Presidio.\n- PII is replaced with entity type placeholders (e.g. "Call me at <PHONE_NUMBER>").\n- If entities is empty, returns empty text immediately without processing.\n- Input: `{ input: string, language: string, entities: string[] }`\n- Output: `{ text: string }`\n\n#### removeBackgroundFromImage\nRemove the background from an image using AI, producing a transparent PNG.\n- Uses the Bria background removal model via fal.ai.\n- Output is re-hosted on the CDN as a PNG with transparency.\n- Input: `{ imageUrl: string }`\n- Output: `{ imageUrl: string }`\n\n#### resizeVideo\nResize a video file\n- Input: `{ videoUrl: string, mode: "fit" | "exact", maxWidth?: number, maxHeight?: number, width?: number, height?: number, strategy?: "pad" | "crop", intermediateAsset?: boolean }`\n- Output: `{ videoUrl: string }`\n\n#### runFromConnectorRegistry\nRun a raw API connector to a third-party service\n- Use the /developer/v2/helpers/connectors endpoint to list available services and actions.\n- Use /developer/v2/helpers/connectors/{serviceId}/{actionId} to get the full input configuration for an action.\n- Use /developer/v2/helpers/connections to list your available OAuth connections.\n- The actionId format is "serviceId/actionId" (e.g., "slack/send-message").\n- Pass a __connectionId to authenticate the request with a specific OAuth connection, otherwise the default will be used (if configured).\n- Input: `{ actionId: string, displayName: string, icon: string, configurationValues: object, __connectionId?: string }`\n- Output: `{ data: object }`\n\n#### runPackagedWorkflow\nRun a packaged workflow ("custom block")\n- From the user\'s perspective, packaged workflows are just ordinary blocks. Behind the scenes, they operate like packages/libraries in a programming language, letting the user execute custom functionality.\n- Some of these packaged workflows are available as part of MindStudio\'s "Standard Library" and available to every user.\n- Available packaged workflows are documented here as individual blocks, but the runPackagedWorkflow block is how they need to be wrapped in order to be executed correctly.\n- Input: `{ appId: string, workflowId: string, inputVariables: object, outputVariables: object, name: string }`\n- Output: `{ data: unknown }`\n\n#### scrapeLinkedInCompany\nScrape public company data from a LinkedIn company page.\n- Requires a LinkedIn company URL (e.g. https://www.linkedin.com/company/mindstudioai).\n- Returns structured company data including description, employees, updates, and similar companies.\n- Input: `{ url: string }`\n- Output: `{ company: unknown }`\n\n#### scrapeLinkedInProfile\nScrape public profile data from a LinkedIn profile page.\n- Requires a LinkedIn profile URL (e.g. https://www.linkedin.com/in/username).\n- Returns structured profile data including experience, education, articles, and activities.\n- Input: `{ url: string }`\n- Output: `{ profile: unknown }`\n\n#### scrapeUrl\nExtract text, HTML, or structured content from one or more web pages.\n- Accepts a single URL or multiple URLs (as a JSON array, comma-separated, or newline-separated).\n- Output format controls the result shape: "text" returns markdown, "html" returns raw HTML, "json" returns structured scraper data.\n- Can optionally capture a screenshot of each page.\n- Input: `{ url: string, service?: "default" | "firecrawl", autoEnhance?: boolean, pageOptions?: { onlyMainContent: boolean, screenshot: boolean, waitFor: number, replaceAllPathsWithAbsolutePaths: boolean, headers: object, removeTags: string[], mobile: boolean } }`\n- Output: `{ content: string | string[] | { text: string, html: string, json?: object, screenshotUrl?: string, metadata?: { title: string, description: string, url: string, image: string } } | { text: string, html: string, json?: object, screenshotUrl?: string, metadata?: { title: string, description: string, url: string, image: string } }[], screenshot?: string }`\n\n#### scrapeXPost\nScrape data from a single X (Twitter) post by URL.\n- Returns structured post data (text, html, optional json/screenshot/metadata).\n- Optionally saves the text content to a variable.\n- Input: `{ url: string }`\n- Output: `{ post: { text: string, html: string, json?: object, screenshotUrl?: string, metadata?: { title: string, description: string, url: string, image: string } } }`\n\n#### scrapeXProfile\nScrape public profile data from an X (Twitter) account by URL.\n- Returns structured profile data.\n- Optionally saves the result to a variable.\n- Input: `{ url: string }`\n- Output: `{ profile: { text: string, html: string, json?: object, screenshotUrl?: string, metadata?: { title: string, description: string, url: string, image: string } } }`\n\n#### screenshotUrl\nCapture a screenshot of a web page as a PNG image.\n- Takes a viewport or full-page screenshot of the given URL.\n- Returns a CDN-hosted PNG image URL.\n- Viewport mode captures only the visible area; fullPage captures the entire scrollable page.\n- You can customize viewport width/height, add a delay, or wait for a CSS selector before capturing.\n- Input: `{ url: string, mode?: "viewport" | "fullPage", width?: number, height?: number, delay?: number, waitFor?: string }`\n- Output: `{ screenshotUrl: string }`\n\n#### searchGmailEmails\nSearch for emails in the connected Gmail account using a Gmail search query. To list recent inbox emails, pass an empty query string.\n- Requires a Google OAuth connection with Gmail readonly scope.\n- Uses Gmail search syntax (e.g. "from:user@example.com", "subject:invoice", "is:unread").\n- To list recent inbox emails, use an empty query string or "in:inbox".\n- Returns up to 100 emails (default 5). The variable receives text or JSON depending on exportType.\n- The direct execution output always returns structured email objects.\n- Input: `{ query: string, connectionId?: string, exportType: "json" | "text", limit: string }`\n- Output: `{ emails: { id: string, subject: string, from: string, to: string, date: string, plainBody: string, htmlBody: string, labels: string }[] }`\n\n#### searchGoogle\nSearch the web using Google and return structured results.\n- Defaults to us/english, but can optionally specify country and/or language.\n- Defaults to any time, but can optionally specify last hour, last day, week, month, or year.\n- Defaults to top 30 results, but can specify 1 to 100 results to return.\n- Input: `{ query: string, exportType: "text" | "json", countryCode?: string, languageCode?: string, dateRange?: "hour" | "day" | "week" | "month" | "year" | "any", numResults?: number }`\n- Output: `{ results: { title: string, description: string, url: string }[] }`\n\n#### searchGoogleImages\nSearch Google Images and return image results with URLs and metadata.\n- Defaults to us/english, but can optionally specify country and/or language.\n- Defaults to any time, but can optionally specify last hour, last day, week, month, or year.\n- Defaults to top 30 results, but can specify 1 to 100 results to return.\n- Input: `{ query: string, exportType: "text" | "json", countryCode?: string, languageCode?: string, dateRange?: "hour" | "day" | "week" | "month" | "year" | "any", numResults?: number }`\n- Output: `{ images: { title: string, imageUrl: string, imageWidth: number, imageHeight: number, thumbnailUrl: string, thumbnailWidth: number, thumbnailHeight: number, source: string, domain: string, link: string, googleUrl: string, position: number }[] }`\n\n#### searchGoogleNews\nSearch Google News for recent news articles matching a query.\n- Defaults to top 30 results, but can specify 1 to 100 results to return.\n- Input: `{ text: string, exportType: "text" | "json", numResults?: number }`\n- Output: `{ articles: { title: string, link: string, date: string, source: { name: string }, snippet?: string }[] }`\n\n#### searchGoogleTrends\nFetch Google Trends data for a search term.\n- date accepts shorthand ("now 1-H", "today 1-m", "today 5-y", etc.) or custom "yyyy-mm-dd yyyy-mm-dd" ranges.\n- data_type controls the shape of returned data: TIMESERIES, GEO_MAP, GEO_MAP_0, RELATED_TOPICS, or RELATED_QUERIES.\n- Input: `{ text: string, hl: string, geo: string, data_type: "TIMESERIES" | "GEO_MAP" | "GEO_MAP_0" | "RELATED_TOPICS" | "RELATED_QUERIES", cat: string, date: string, ts: string }`\n- Output: `{ trends: object }`\n\n#### searchPerplexity\nSearch the web using the Perplexity API and return structured results.\n- Defaults to US results. Use countryCode (ISO code) to filter by country.\n- Returns 10 results by default, configurable from 1 to 20.\n- The variable receives text or JSON depending on exportType. The direct execution output always returns structured results.\n- Input: `{ query: string, exportType: "text" | "json", countryCode?: string, numResults?: number }`\n- Output: `{ results: { title: string, description: string, url: string }[] }`\n\n#### sendEmail\nSend an email to one or more recipient addresses.\n- Use the "to" field to send to specific email addresses directly. For v2 apps, recipients must be verified users in the app\'s user table.\n- Alternatively, recipient email addresses can be resolved from OAuth connections configured by the app creator via connectionId. The user running the workflow does not specify the recipient directly.\n- If the body is a URL to a hosted HTML file on the CDN, the HTML is fetched and used as the email body.\n- When generateHtml is enabled, the body text is converted to a styled HTML email using an AI model.\n- connectionId can be a comma-separated list to send to multiple recipients.\n- The special connectionId "trigger_email" uses the email address that triggered the workflow.\n- Input: `{ subject: string, body: string, to?: string | string[], connectionId?: string, generateHtml?: boolean, generateHtmlInstructions?: string, generateHtmlModelOverride?: { model: string, temperature: number, maxResponseTokens: number, ignorePreamble?: boolean, userMessagePreprocessor?: { dataSource?: string, messageTemplate?: string, maxResults?: number, enabled?: boolean, shouldInherit?: boolean }, preamble?: string, multiModelEnabled?: boolean, editResponseEnabled?: boolean, config?: object }, attachments?: string[] }`\n- Output: `{ recipients: string[] }`\n\n#### sendGmailDraft\nSend an existing draft from the connected Gmail account.\n- Requires a Google OAuth connection with Gmail compose scope.\n- The draft is sent and removed from the Drafts folder.\n- Use the draft ID returned by the Create Gmail Draft or List Gmail Drafts steps.\n- Input: `{ draftId: string, connectionId?: string }`\n- Output: `unknown`\n\n#### sendGmailMessage\nSend an email from the connected Gmail account.\n- Requires a Google OAuth connection with Gmail compose scope.\n- messageType controls the body format: "plain" for plain text, "html" for raw HTML, "markdown" for auto-converted markdown.\n- Input: `{ to: string, subject: string, message: string, connectionId?: string, messageType: "plain" | "html" | "markdown" }`\n- Output: `{ messageId: string }`\n\n#### sendSlackDirectMessage\nSend a direct message to a Slack user via a connected bot.\n- The user is responsible for connecting their Slack workspace\n- The recipient is identified by their Slack user ID\n- Supports both simple text messages and Slack blocks messages\n- Text messages can use limited markdown (slack-only formatting\u2014e.g., headers are just rendered as bold)\n- Input: `{ slackUserId: string, messageType: "string" | "blocks", message: string, connectionId?: string }`\n- Output: `unknown`\n\n#### sendSMS\nSend an SMS or MMS message to a phone number configured via OAuth connection.\n- User is responsible for configuring the connection to the number (MindStudio requires double opt-in to prevent spam)\n- If mediaUrls are provided, the message is sent as MMS instead of SMS\n- MMS supports up to 10 media URLs (images, video, audio, PDF) with a 5MB limit per file\n- MMS is only supported on US and Canadian carriers; international numbers will receive SMS only (media silently dropped)\n- Input: `{ body: string, connectionId?: string, mediaUrls?: string[] }`\n- Output: `unknown`\n\n#### setGmailReadStatus\nMark one or more Gmail emails as read or unread.\n- Requires a Google OAuth connection with Gmail modify scope.\n- Accepts one or more message IDs as a comma-separated string or array.\n- Set markAsRead to true to mark as read, false to mark as unread.\n- Input: `{ messageIds: string, markAsRead: boolean, connectionId?: string }`\n- Output: `unknown`\n\n#### setRunTitle\nSet the title of the agent run for the user\'s history\n- Input: `{ title: string }`\n- Output: `unknown`\n\n#### setVariable\nExplicitly set a variable to a given value.\n- Useful for bootstrapping global variables or setting constants.\n- The variable name and value both support variable interpolation.\n- The type field is a UI hint only (controls input widget in the editor).\n- Input: `{ value: string | string[] }`\n- Output: `object`\n\n#### telegramEditMessage\nEdit a previously sent Telegram message. Use with the message ID returned by Send Telegram Message.\n- Only text messages sent by the bot can be edited.\n- The messageId is returned by the Send Telegram Message step.\n- Common pattern: send a "Processing..." message, do work, then edit it with the result.\n- Input: `{ botToken: string, chatId: string, messageId: string, text: string }`\n- Output: `unknown`\n\n#### telegramReplyToMessage\nSend a reply to a specific Telegram message. The reply will be visually threaded in the chat.\n- Use the rawMessage.message_id from the incoming trigger variables to reply to the user\'s message.\n- Especially useful in group chats where replies provide context.\n- Returns the sent message ID, which can be used with Edit Telegram Message.\n- Input: `{ botToken: string, chatId: string, replyToMessageId: string, text: string }`\n- Output: `{ messageId: number }`\n\n#### telegramSendAudio\nSend an audio file to a Telegram chat as music or a voice note via a bot.\n- "audio" mode sends as a standard audio file. "voice" mode sends as a voice message (re-uploads the file for large file support).\n- Input: `{ botToken: string, chatId: string, audioUrl: string, mode: "audio" | "voice", caption?: string }`\n- Output: `unknown`\n\n#### telegramSendFile\nSend a document/file to a Telegram chat via a bot.\n- Input: `{ botToken: string, chatId: string, fileUrl: string, caption?: string }`\n- Output: `unknown`\n\n#### telegramSendImage\nSend an image to a Telegram chat via a bot.\n- Input: `{ botToken: string, chatId: string, imageUrl: string, caption?: string }`\n- Output: `unknown`\n\n#### telegramSendMessage\nSend a text message to a Telegram chat via a bot.\n- Messages are sent using MarkdownV2 formatting. Special characters are auto-escaped.\n- botToken format is "botId:token" \u2014 both parts are required.\n- Returns the sent message ID, which can be used with Edit Telegram Message to update the message later.\n- Input: `{ botToken: string, chatId: string, text: string }`\n- Output: `{ messageId: number }`\n\n#### telegramSendVideo\nSend a video to a Telegram chat via a bot.\n- Input: `{ botToken: string, chatId: string, videoUrl: string, caption?: string }`\n- Output: `unknown`\n\n#### telegramSetTyping\nShow the "typing..." indicator in a Telegram chat via a bot.\n- The typing indicator automatically expires after a few seconds. Use this right before sending a message for a natural feel.\n- Input: `{ botToken: string, chatId: string }`\n- Output: `unknown`\n\n#### textToSpeech\nGenerate an audio file from provided text using a speech model.\n- The text field contains the exact words to be spoken (not instructions).\n- In foreground mode, the audio is displayed to the user. In background mode, the URL is saved to a variable.\n- Input: `{ text: string, intermediateAsset?: boolean, speechModelOverride?: { model: string, config?: object } }`\n- Output: `{ audioUrl: string }`\n\n#### transcribeAudio\nConvert an audio file to text using a transcription model.\n- The prompt field provides optional context to improve transcription accuracy (e.g. language, speaker names, domain).\n- Input: `{ audioUrl: string, prompt: string, transcriptionModelOverride?: { model: string, config?: object } }`\n- Output: `{ text: string }`\n\n#### trimMedia\nTrim an audio or video clip\n- Input: `{ inputUrl: string, start?: number | string, duration?: string | number, intermediateAsset?: boolean }`\n- Output: `{ mediaUrl: string }`\n\n#### updateGmailLabels\nAdd or remove labels on Gmail messages, identified by message IDs or a search query.\n- Requires a Google OAuth connection with Gmail modify scope.\n- Provide either a query (Gmail search syntax) or explicit messageIds to target messages.\n- Label IDs can be label names or Gmail label IDs \u2014 names are resolved automatically.\n- Input: `{ query: string, connectionId?: string, messageIds: string, addLabelIds: string, removeLabelIds: string }`\n- Output: `{ updatedMessageIds: string[] }`\n\n#### uploadDataSourceDocument\nUpload a file into an existing data source from a URL or raw text content.\n- If "file" is a single URL, the file is downloaded from that URL and uploaded.\n- If "file" is any other string, a .txt document is created from that content and uploaded.\n- The block waits (polls) for processing to complete before transitioning, up to 5 minutes.\n- Once processing finishes, vectors are loaded into Milvus so the data source is immediately queryable.\n- Supported file types (when using a URL) are the same as the data source upload UI (PDF, DOCX, TXT, etc.).\n- Input: `{ dataSourceId: string, file: string, fileName: string }`\n- Output: `unknown`\n\n#### upscaleImage\nIncrease the resolution of an image using AI upscaling.\n- Output is re-hosted on the CDN as a PNG.\n- Input: `{ imageUrl: string, targetResolution: "2k" | "4k" | "8k", engine: "standard" | "pro" }`\n- Output: `{ imageUrl: string }`\n\n#### upscaleVideo\nUpscale a video file\n- Input: `{ videoUrl: string, targetResolution: "720p" | "1080p" | "2K" | "4K", engine: "standard" | "pro" | "ultimate" | "flashvsr" | "seedance" | "seedvr2" | "runwayml/upscale-v1", intermediateAsset?: boolean }`\n- Output: `{ videoUrl: string }`\n\n#### videoFaceSwap\nSwap faces in a video file\n- Input: `{ videoUrl: string, faceImageUrl: string, targetIndex: number, engine: string, intermediateAsset?: boolean }`\n- Output: `{ videoUrl: string }`\n\n#### videoRemoveBackground\nRemove or replace background from a video\n- Input: `{ videoUrl: string, newBackground: "transparent" | "image", newBackgroundImageUrl?: string, engine: string, intermediateAsset?: boolean }`\n- Output: `{ videoUrl: string }`\n\n#### videoRemoveWatermark\nRemove a watermark from a video\n- Input: `{ videoUrl: string, engine: string, intermediateAsset?: boolean }`\n- Output: `{ videoUrl: string }`\n\n#### watermarkImage\nOverlay a watermark image onto another image.\n- The watermark is placed at the specified corner with configurable padding and width.\n- Input: `{ imageUrl: string, watermarkImageUrl: string, corner: "top-left" | "top-right" | "bottom-left" | "bottom-right", paddingPx: number, widthPx: number, intermediateAsset?: boolean }`\n- Output: `{ imageUrl: string }`\n\n#### watermarkVideo\nAdd an image watermark to a video\n- Input: `{ videoUrl: string, imageUrl: string, corner: "top-left" | "top-right" | "bottom-left" | "bottom-right", paddingPx: number, widthPx: number, intermediateAsset?: boolean }`\n- Output: `{ videoUrl: string }`\n\n### ActiveCampaign\n\n#### activeCampaignAddNote\nAdd a note to an existing contact in ActiveCampaign.\n- Requires an ActiveCampaign OAuth connection (connectionId).\n- The contact must already exist \u2014 use the contact ID from a previous create or search step.\n- Input: `{ contactId: string, note: string, connectionId?: string }`\n- Output: `unknown`\n\n#### activeCampaignCreateContact\nCreate or sync a contact in ActiveCampaign.\n- Requires an ActiveCampaign OAuth connection (connectionId).\n- If a contact with the email already exists, it may be updated depending on ActiveCampaign settings.\n- Custom fields are passed as a key-value map where keys are field IDs.\n- Input: `{ email: string, firstName: string, lastName: string, phone: string, accountId: string, customFields: object, connectionId?: string }`\n- Output: `{ contactId: string }`\n\n### Airtable\n\n#### airtableCreateUpdateRecord\nCreate a new record or update an existing record in an Airtable table.\n- If recordId is provided, updates that record. Otherwise, creates a new one.\n- When updating with updateMode "onlySpecified", unspecified fields are left as-is. With "all", unspecified fields are cleared.\n- Array fields (e.g. multipleAttachments) accept arrays of values.\n- Input: `{ connectionId?: string, baseId: string, tableId: string, recordId?: string, updateMode?: "onlySpecified" | "all", fields: unknown, recordData: object }`\n- Output: `{ recordId: string }`\n\n#### airtableDeleteRecord\nDelete a record from an Airtable table by its record ID.\n- Requires an active Airtable OAuth connection (connectionId).\n- Silently succeeds if the record does not exist.\n- Input: `{ connectionId?: string, baseId: string, tableId: string, recordId: string }`\n- Output: `{ deleted: boolean }`\n\n#### airtableGetRecord\nFetch a single record from an Airtable table by its record ID.\n- Requires an active Airtable OAuth connection (connectionId).\n- If the record is not found, returns a string message instead of a record object.\n- Input: `{ connectionId?: string, baseId: string, tableId: string, recordId: string }`\n- Output: `{ record: { id: string, createdTime: string, fields: object } | null }`\n\n#### airtableGetTableRecords\nFetch multiple records from an Airtable table with optional pagination.\n- Requires an active Airtable OAuth connection (connectionId).\n- Default limit is 100 records. Maximum is 1000.\n- When outputFormat is \'csv\', the variable receives CSV text. The direct execution output always returns parsed records.\n- Input: `{ connectionId?: string, baseId: string, tableId: string, outputFormat?: "json" | "csv", limit?: number }`\n- Output: `{ records: { id: string, createdTime: string, fields: object }[] }`\n\n### Apollo\n\n#### enrichPerson\nLook up professional information about a person using Apollo.io. Search by ID, name, LinkedIn URL, email, or domain.\n- At least one search parameter must be provided.\n- Returns enriched data from Apollo including contact details, employment info, and social profiles.\n- Input: `{ params: { id: string, name: string, linkedinUrl: string, email: string, domain: string } }`\n- Output: `{ data: unknown }`\n\n#### peopleSearch\nSearch for people matching specific criteria using Apollo.io. Supports natural language queries and advanced filters.\n- Can use a natural language "smartQuery" which is converted to Apollo search parameters by an AI model.\n- Advanced params can override or supplement the smart query results.\n- Optionally enriches returned people and/or their organizations for additional detail.\n- Results are paginated. Use limit and page to control the result window.\n- Input: `{ smartQuery: string, enrichPeople: boolean, enrichOrganizations: boolean, limit: string, page: string, params: { personTitles: string, includeSimilarTitles: string, qKeywords: string, personLocations: string, personSeniorities: string, organizationLocations: string, qOrganizationDomainsList: string, contactEmailStatus: string, organizationNumEmployeesRanges: string, revenueRangeMin: string, revenueRangeMax: string, currentlyUsingAllOfTechnologyUids: string, currentlyUsingAnyOfTechnologyUids: string, currentlyNotUsingAnyOfTechnologyUids: string } }`\n- Output: `{ results: unknown }`\n\n### Coda\n\n#### codaCreateUpdatePage\nCreate a new page or update an existing page in a Coda document.\n- Requires a Coda OAuth connection (connectionId).\n- If pageData.pageId is provided, updates that page. Otherwise, creates a new one.\n- Page content is provided as markdown and converted to Coda\'s canvas format.\n- When updating, insertionMode controls how content is applied (default: \'append\').\n- Input: `{ connectionId?: string, pageData: { docId: string, pageId?: string, name: string, subtitle: string, iconName: string, imageUrl: string, parentPageId?: string, pageContent: string | unknown, contentUpdate?: unknown, insertionMode?: string } }`\n- Output: `{ pageId: string }`\n\n#### codaCreateUpdateRow\nCreate a new row or update an existing row in a Coda table.\n- Requires a Coda OAuth connection (connectionId).\n- If rowId is provided, updates that row. Otherwise, creates a new one.\n- Row data keys are column IDs. Empty values are excluded.\n- Input: `{ connectionId?: string, docId: string, tableId: string, rowId?: string, rowData: object }`\n- Output: `{ rowId: string }`\n\n#### codaFindRow\nSearch for a row in a Coda table by matching column values.\n- Requires a Coda OAuth connection (connectionId).\n- Returns the first row matching all specified column values, or null if no match.\n- Search criteria in rowData are ANDed together.\n- Input: `{ connectionId?: string, docId: string, tableId: string, rowData: object }`\n- Output: `{ row: { id: string, values: object } | null }`\n\n#### codaGetPage\nExport and read the contents of a page from a Coda document.\n- Requires a Coda OAuth connection (connectionId).\n- Page export is asynchronous on Coda\'s side \u2014 there may be a brief delay while it processes.\n- If a page was just created in a prior step, there is an automatic 20-second retry if the first export attempt fails.\n- Input: `{ connectionId?: string, docId: string, pageId: string, outputFormat?: "html" | "markdown" }`\n- Output: `{ content: string }`\n\n#### codaGetTableRows\nFetch rows from a Coda table with optional pagination.\n- Requires a Coda OAuth connection (connectionId).\n- Default limit is 10000 rows. Rows are fetched in pages of 500.\n- When outputFormat is \'csv\', the variable receives CSV text. The direct execution output always returns parsed rows.\n- Input: `{ connectionId?: string, docId: string, tableId: string, limit?: number | string, outputFormat?: "json" | "csv" }`\n- Output: `{ rows: { id: string, values: object }[] }`\n\n### Facebook\n\n#### scrapeFacebookPage\nScrape a Facebook page\n- Input: `{ pageUrl: string }`\n- Output: `{ data: unknown }`\n\n#### scrapeFacebookPosts\nGet all the posts for a Facebook page\n- Input: `{ pageUrl: string }`\n- Output: `{ data: unknown }`\n\n### Gmail\n\n#### deleteGmailEmail\nMove an email to trash in the connected Gmail account (recoverable delete).\n- Requires a Google OAuth connection with Gmail modify scope.\n- Uses trash (recoverable) rather than permanent delete.\n- Input: `{ messageId: string, connectionId?: string }`\n- Output: `unknown`\n\n#### getGmailDraft\nRetrieve a specific draft from Gmail by draft ID.\n- Requires a Google OAuth connection with Gmail readonly scope.\n- Returns the draft content including subject, recipients, sender, and body.\n- Input: `{ draftId: string, connectionId?: string }`\n- Output: `{ draftId: string, messageId: string, subject: string, to: string, from: string, body: string }`\n\n#### getGmailEmail\nRetrieve a specific email from Gmail by message ID.\n- Requires a Google OAuth connection with Gmail readonly scope.\n- Returns the email subject, sender, recipient, date, body (plain text preferred, falls back to HTML), and labels.\n- Input: `{ messageId: string, connectionId?: string }`\n- Output: `{ messageId: string, subject: string, from: string, to: string, date: string, body: string, labels: string }`\n\n#### listGmailDrafts\nList drafts in the connected Gmail account.\n- Requires a Google OAuth connection with Gmail readonly scope.\n- Returns up to 50 drafts (default 10).\n- The variable receives text or JSON depending on exportType.\n- Input: `{ connectionId?: string, limit?: string, exportType: "json" | "text" }`\n- Output: `{ drafts: { draftId: string, messageId: string, subject: string, to: string, snippet: string }[] }`\n\n#### replyToGmailEmail\nReply to an existing email in Gmail. The reply is threaded under the original message.\n- Requires a Google OAuth connection with Gmail compose and readonly scopes.\n- The reply is sent to the original sender and threaded under the original message.\n- messageType controls the body format: "plain", "html", or "markdown".\n- Input: `{ messageId: string, message: string, messageType: "plain" | "html" | "markdown", connectionId?: string }`\n- Output: `{ messageId: string }`\n\n### Google\n\n#### createGoogleDoc\nCreate a new Google Document and optionally populate it with content.\n- textType determines how the text field is interpreted: "plain" for plain text, "html" for HTML markup, "markdown" for Markdown.\n- Input: `{ title: string, text: string, connectionId?: string, textType: "plain" | "html" | "markdown" }`\n- Output: `{ documentUrl: string }`\n\n#### createGoogleSheet\nCreate a new Google Spreadsheet and populate it with CSV data.\n- Input: `{ title: string, text: string, connectionId?: string }`\n- Output: `{ spreadsheetUrl: string }`\n\n#### deleteGoogleSheetRows\nDelete a range of rows from a Google Spreadsheet.\n- Requires a Google OAuth connection with Drive scope.\n- startRow and endRow are 1-based row numbers (inclusive).\n- If sheetName is omitted, operates on the first sheet.\n- Input: `{ documentId: string, sheetName?: string, startRow: string, endRow: string, connectionId?: string }`\n- Output: `unknown`\n\n#### fetchGoogleDoc\nFetch the contents of an existing Google Document.\n- exportType controls the output format: "html" for HTML markup, "markdown" for Markdown, "json" for structured JSON, "plain" for plain text.\n- Input: `{ documentId: string, connectionId?: string, exportType: "html" | "markdown" | "json" | "plain" }`\n- Output: `{ content: string }`\n\n#### fetchGoogleSheet\nFetch contents of a Google Spreadsheet range.\n- range uses A1 notation (e.g. "Sheet1!A1:C10"). Omit to fetch the entire first sheet.\n- exportType controls the output format: "csv" for comma-separated values, "json" for structured JSON.\n- Input: `{ spreadsheetId: string, range: string, connectionId?: string, exportType: "csv" | "json" }`\n- Output: `{ content: string }`\n\n#### getGoogleSheetInfo\nGet metadata about a Google Spreadsheet including sheet names, row counts, and column counts.\n- Requires a Google OAuth connection with Drive scope.\n- Returns the spreadsheet title and a list of all sheets with their dimensions.\n- Input: `{ documentId: string, connectionId?: string }`\n- Output: `{ title: string, sheets: { sheetId: number, title: string, rowCount: number, columnCount: number }[] }`\n\n#### updateGoogleDoc\nUpdate the contents of an existing Google Document.\n- operationType controls how content is applied: "addToTop" prepends, "addToBottom" appends, "overwrite" replaces all content.\n- textType determines how the text field is interpreted: "plain" for plain text, "html" for HTML markup, "markdown" for Markdown.\n- Input: `{ documentId: string, connectionId?: string, text: string, textType: "plain" | "html" | "markdown", operationType: "addToTop" | "addToBottom" | "overwrite" }`\n- Output: `{ documentUrl: string }`\n\n#### updateGoogleSheet\nUpdate a Google Spreadsheet with new data.\n- operationType controls how data is written: "addToBottom" appends rows, "overwrite" replaces all data, "range" writes to a specific cell range.\n- Data should be provided as CSV in the text field.\n- Input: `{ text: string, connectionId?: string, spreadsheetId: string, range: string, operationType: "addToBottom" | "overwrite" | "range" }`\n- Output: `{ spreadsheetUrl: string }`\n\n### Google Calendar\n\n#### createGoogleCalendarEvent\nCreate a new event on a Google Calendar.\n- Requires a Google OAuth connection with Calendar events scope.\n- Date/time values must be ISO 8601 format (e.g. "2025-07-02T10:00:00-07:00").\n- Attendees are specified as one email address per line in a single string.\n- Set addMeetLink to true to automatically attach a Google Meet video call.\n- Input: `{ connectionId?: string, summary: string, description?: string, location?: string, startDateTime: string, endDateTime: string, attendees?: string, addMeetLink?: boolean, calendarId?: string }`\n- Output: `{ eventId: string, htmlLink: string }`\n\n#### deleteGoogleCalendarEvent\nRetrieve a specific event from a Google Calendar by event ID.\n- Requires a Google OAuth connection with Calendar events scope.\n- The variable receives JSON or XML-like text depending on exportType. The direct execution output always returns the structured event.\n- Input: `{ connectionId?: string, eventId: string, calendarId?: string }`\n- Output: `unknown`\n\n#### getGoogleCalendarEvent\nRetrieve a specific event from a Google Calendar by event ID.\n- Requires a Google OAuth connection with Calendar events scope.\n- The variable receives JSON or XML-like text depending on exportType. The direct execution output always returns the structured event.\n- Input: `{ connectionId?: string, eventId: string, exportType: "json" | "text", calendarId?: string }`\n- Output: `{ event: { id?: string | null, status?: string | null, htmlLink?: string | null, created?: string | null, updated?: string | null, summary?: string | null, description?: string | null, location?: string | null, organizer?: { displayName?: string | null, email?: string | null } | null, start?: { dateTime?: string | null, timeZone?: string | null } | null, end?: { dateTime?: string | null, timeZone?: string | null } | null, attendees?: ({ displayName?: string | null, email?: string | null, responseStatus?: string | null })[] | null } }`\n\n#### listGoogleCalendarEvents\nList upcoming events from a Google Calendar, ordered by start time.\n- Requires a Google OAuth connection with Calendar events scope.\n- Only returns future events (timeMin = now).\n- The variable receives JSON or XML-like text depending on exportType. The direct execution output always returns structured events.\n- Input: `{ connectionId?: string, limit: number, exportType: "json" | "text", calendarId?: string }`\n- Output: `{ events: ({ id?: string | null, status?: string | null, htmlLink?: string | null, created?: string | null, updated?: string | null, summary?: string | null, description?: string | null, location?: string | null, organizer?: { displayName?: string | null, email?: string | null } | null, start?: { dateTime?: string | null, timeZone?: string | null } | null, end?: { dateTime?: string | null, timeZone?: string | null } | null, attendees?: ({ displayName?: string | null, email?: string | null, responseStatus?: string | null })[] | null })[] }`\n\n#### searchGoogleCalendarEvents\nSearch for events in a Google Calendar by keyword, date range, or both.\n- Requires a Google OAuth connection with Calendar events scope.\n- Supports keyword search via "query" and date filtering via "timeMin"/"timeMax" (ISO 8601 format).\n- Unlike "List Events" which only shows future events, this allows searching past events too.\n- Input: `{ query?: string, timeMin?: string, timeMax?: string, calendarId?: string, limit?: number, exportType: "json" | "text", connectionId?: string }`\n- Output: `{ events: ({ id?: string | null, status?: string | null, htmlLink?: string | null, created?: string | null, updated?: string | null, summary?: string | null, description?: string | null, location?: string | null, organizer?: { displayName?: string | null, email?: string | null } | null, start?: { dateTime?: string | null, timeZone?: string | null } | null, end?: { dateTime?: string | null, timeZone?: string | null } | null, attendees?: ({ displayName?: string | null, email?: string | null, responseStatus?: string | null })[] | null })[] }`\n\n#### updateGoogleCalendarEvent\nUpdate an existing event on a Google Calendar. Only specified fields are changed.\n- Requires a Google OAuth connection with Calendar events scope.\n- Fetches the existing event first, then applies only the provided updates. Omitted fields are left unchanged.\n- Attendees are specified as one email address per line, and replace the entire attendee list.\n- Input: `{ connectionId?: string, eventId: string, summary?: string, description?: string, location?: string, startDateTime?: string, endDateTime?: string, attendees?: string, calendarId?: string }`\n- Output: `{ eventId: string, htmlLink: string }`\n\n### Google Drive\n\n#### getGoogleDriveFile\nDownload a file from Google Drive and rehost it on the CDN. Returns a public CDN URL.\n- Requires a Google OAuth connection with Drive scope.\n- Google-native files (Docs, Sheets, Slides) cannot be downloaded \u2014 use dedicated steps instead.\n- Maximum file size: 200MB.\n- The file is downloaded and re-uploaded to the CDN; the returned URL is publicly accessible.\n- Input: `{ fileId: string, connectionId?: string }`\n- Output: `{ url: string, name: string, mimeType: string, size: number }`\n\n#### listGoogleDriveFiles\nList files in a Google Drive folder.\n- Requires a Google OAuth connection with Drive scope.\n- If folderId is omitted, lists files in the root folder.\n- Returns file metadata including name, type, size, and links.\n- Input: `{ folderId?: string, limit?: number, connectionId?: string, exportType: "json" | "text" }`\n- Output: `{ files: { id: string, name: string, mimeType: string, size: string, webViewLink: string, createdTime: string, modifiedTime: string }[] }`\n\n#### searchGoogleDrive\nSearch for files in Google Drive by keyword.\n- Requires a Google OAuth connection with Drive scope.\n- Searches file content and names using Google Drive\'s fullText search.\n- Input: `{ query: string, limit?: number, connectionId?: string, exportType: "json" | "text" }`\n- Output: `{ files: { id: string, name: string, mimeType: string, size: string, webViewLink: string, createdTime: string, modifiedTime: string }[] }`\n\n### HubSpot\n\n#### hubspotCreateCompany\nCreate a new company or update an existing one in HubSpot. Matches by domain.\n- Requires a HubSpot OAuth connection (connectionId).\n- If a company with the given domain already exists, it is updated. Otherwise, a new one is created.\n- Property values are type-checked against enabledProperties before being sent to HubSpot.\n- Input: `{ connectionId?: string, company: { domain: string, name: string }, enabledProperties: ({ label: string, value: string, type: "string" | "number" | "bool" })[] }`\n- Output: `{ companyId: string }`\n\n#### hubspotCreateContact\nCreate a new contact or update an existing one in HubSpot. Matches by email address.\n- Requires a HubSpot OAuth connection (connectionId).\n- If a contact with the given email already exists, it is updated. Otherwise, a new one is created.\n- If companyDomain is provided, the contact is associated with that company (creating the company if needed).\n- Property values are type-checked against enabledProperties before being sent to HubSpot.\n- Input: `{ connectionId?: string, contact: { email: string, firstname: string, lastname: string }, enabledProperties: ({ label: string, value: string, type: "string" | "number" | "bool" })[], companyDomain: string }`\n- Output: `{ contactId: string }`\n\n#### hubspotGetCompany\nLook up a HubSpot company by domain name or company ID.\n- Requires a HubSpot OAuth connection (connectionId).\n- Returns null if the company is not found.\n- When searching by domain, performs a search query then fetches the full company record.\n- Use additionalProperties to request specific HubSpot properties beyond the defaults.\n- Input: `{ connectionId?: string, searchBy: "domain" | "id", companyDomain: string, companyId: string, additionalProperties: string[] }`\n- Output: `{ company: { id: string, properties: object, createdAt: string, updatedAt: string, archived: boolean } | null }`\n\n#### hubspotGetContact\nLook up a HubSpot contact by email address or contact ID.\n- Requires a HubSpot OAuth connection (connectionId).\n- Returns null if the contact is not found.\n- Use additionalProperties to request specific HubSpot properties beyond the defaults.\n- Input: `{ connectionId?: string, searchBy: "email" | "id", contactEmail: string, contactId: string, additionalProperties: string[] }`\n- Output: `{ contact: { id: string, properties: object, createdAt: string, updatedAt: string, archived: boolean } | null }`\n\n### Hunter.io\n\n#### hunterApiCompanyEnrichment\nLook up company information by domain using Hunter.io.\n- Returns company name, description, location, industry, size, technologies, and more.\n- If the domain input is a full URL, the hostname is automatically extracted.\n- Returns null if the company is not found.\n- Input: `{ domain: string }`\n- Output: `{ data: { name: string, domain: string, description: string | null, country: string | null, state: string | null, city: string | null, industry: string | null, employees_range: string | null, logo_url: string | null, technologies: string[] } | null }`\n\n#### hunterApiDomainSearch\nSearch for email addresses associated with a domain using Hunter.io.\n- If the domain input is a full URL, the hostname is automatically extracted.\n- Returns a list of email addresses found for the domain along with organization info.\n- Input: `{ domain: string }`\n- Output: `{ data: { domain: string, disposable: boolean, webmail: boolean, accept_all: boolean, pattern: string, organization: string, country: string | null, state: string | null, emails: ({ value: string, type: string, confidence: number, first_name: string | null, last_name: string | null, position: string | null, seniority: string | null, department: string | null, linkedin: string | null, twitter: string | null, phone_number: string | null })[], linked_domains: string[] } }`\n\n#### hunterApiEmailFinder\nFind an email address for a specific person at a domain using Hunter.io.\n- Requires a first name, last name, and domain.\n- If the domain input is a full URL, the hostname is automatically extracted.\n- Returns the most likely email address with a confidence score.\n- Input: `{ domain: string, firstName: string, lastName: string }`\n- Output: `{ data: { first_name: string, last_name: string, email: string, score: number, domain: string, accept_all: boolean, position: string | null, twitter: string | null, linkedin_url: string | null, phone_number: string | null, company: string | null, sources: { domain: string, uri: string, extracted_on: string }[] } }`\n\n#### hunterApiEmailVerification\nVerify whether an email address is valid and deliverable using Hunter.io.\n- Checks email format, MX records, SMTP server, and mailbox deliverability.\n- Returns a status ("valid", "invalid", "accept_all", "webmail", "disposable", "unknown") and a score.\n- Input: `{ email: string }`\n- Output: `{ data: { status: string, result: string, score: number, email: string, regexp: boolean, gibberish: boolean, disposable: boolean, webmail: boolean, mx_records: boolean, smtp_server: boolean, smtp_check: boolean, accept_all: boolean, block: boolean, sources: { domain: string, uri: string, extracted_on: string }[] } }`\n\n#### hunterApiPersonEnrichment\nLook up professional information about a person by their email address using Hunter.io.\n- Returns name, job title, social profiles, and company information.\n- If the person is not found, returns an object with an error message instead of throwing.\n- Input: `{ email: string }`\n- Output: `{ data: { first_name: string, last_name: string, email: string, position: string | null, seniority: string | null, department: string | null, linkedin_url: string | null, twitter: string | null, phone_number: string | null, company: { name: string, domain: string, industry: string | null } | null } | { error: string } }`\n\n### Instagram\n\n#### scrapeInstagramComments\nGet all the comments for an Instagram post\n- Input: `{ postUrl: string, resultsLimit: string }`\n- Output: `{ data: unknown }`\n\n#### scrapeInstagramMentions\nScrape an Instagram profile\'s mentions\n- Input: `{ profileUrl: string, resultsLimit: string }`\n- Output: `{ data: unknown }`\n\n#### scrapeInstagramPosts\nGet all the posts for an Instagram profile\n- Input: `{ profileUrl: string, resultsLimit: string, onlyPostsNewerThan: string }`\n- Output: `{ data: unknown }`\n\n#### scrapeInstagramProfile\nScrape an Instagram profile\n- Input: `{ profileUrl: string }`\n- Output: `{ data: unknown }`\n\n#### scrapeInstagramReels\nGet all the reels for an Instagram profile\n- Input: `{ profileUrl: string, resultsLimit: string }`\n- Output: `{ data: unknown }`\n\n### LinkedIn\n\n#### postToLinkedIn\nCreate a post on LinkedIn from the connected account.\n- Requires a LinkedIn OAuth connection (connectionId).\n- Supports text posts, image posts, video posts, document posts, and article posts.\n- Attach one media type per post: image, video, document, or article.\n- Documents support PDF, PPT, PPTX, DOC, DOCX (max 100MB, 300 pages). Displays as a slideshow carousel.\n- Articles create a link preview with optional custom title, description, and thumbnail.\n- Visibility controls who can see the post.\n- Input: `{ message: string, visibility: "PUBLIC" | "CONNECTIONS", imageUrl?: string, videoUrl?: string, documentUrl?: string, articleUrl?: string, titleText?: string, descriptionText?: string, connectionId?: string }`\n- Output: `unknown`\n\n### Meta Threads\n\n#### scrapeMetaThreadsProfile\nScrape a Meta Threads profile\n- Input: `{ profileUrl: string }`\n- Output: `{ data: unknown }`\n\n### Notion\n\n#### notionCreatePage\nCreate a new page in Notion as a child of an existing page.\n- Requires a Notion OAuth connection (connectionId).\n- Content is provided as markdown and converted to Notion blocks (headings, paragraphs, lists, code, quotes).\n- The page is created as a child of the specified parent page (pageId).\n- Input: `{ pageId: string, content: string, title: string, connectionId?: string }`\n- Output: `{ pageId: string, pageUrl: string }`\n\n#### notionUpdatePage\nUpdate the content of an existing Notion page.\n- Requires a Notion OAuth connection (connectionId).\n- Content is provided as markdown and converted to Notion blocks.\n- "append" mode adds content to the end of the page. "overwrite" mode deletes all existing blocks first.\n- Input: `{ pageId: string, content: string, mode: "append" | "overwrite", connectionId?: string }`\n- Output: `{ pageId: string, pageUrl: string }`\n\n### X\n\n#### postToX\nCreate a post on X (Twitter) from the connected account.\n- Requires an X OAuth connection (connectionId).\n- Maximum 280 characters of text.\n- Optionally attach up to 4 media items (images, GIFs, or videos) via mediaUrls.\n- Media URLs must be publicly accessible. The service fetches and uploads them to X.\n- Supported formats: JPEG, PNG, GIF, WEBP, MP4. Images up to 5MB, videos up to 512MB.\n- Input: `{ text: string, connectionId?: string, mediaUrls?: string[] }`\n- Output: `unknown`\n\n#### searchXPosts\nSearch recent X (Twitter) posts matching a query.\n- Searches only the past 7 days of posts.\n- Query supports X API v2 search operators (up to 512 characters).\nAvailable search operators in query:\n| Operator | Description |\n| -----------------| -------------------------------------------------|\n| from: | Posts from a specific user (e.g., from:elonmusk) |\n| to: | Posts sent to a specific user (e.g., to:NASA) |\n| @ | Mentions a user (e.g., @openai) |\n| # | Hashtag search (e.g., #AI) |\n| is:retweet | Filters retweets |\n| is:reply | Filters replies |\n| has:media | Posts containing media (images, videos, or GIFs) |\n| has:links | Posts containing URLs |\n| lang: | Filters by language (e.g., lang:en) |\n| - | Excludes specific terms (e.g., -spam) |\n| () | Groups terms or operators (e.g., (AI OR ML)) |\n| AND, OR, NOT | Boolean logic for combining or excluding terms |\nConjunction-Required Operators (must be combined with a standalone operator):\n| Operator | Description |\n| ------------ | -----------------------------------------------|\n| has:media | Posts containing media (images, videos, or GIFs) |\n| has:links | Posts containing URLs |\n| is:retweet | Filters retweets |\n| is:reply | Filters replies |\nFor example, has:media alone is invalid, but #AI has:media is valid.\n- Input: `{ query: string, scope: "recent" | "all", options: { startTime?: string, endTime?: string, maxResults?: number } }`\n- Output: `{ posts: { id: string, authorId: string, dateCreated: string, text: string, stats: { retweets: number, replies: number, likes: number } }[] }`\n\n### YouTube\n\n#### fetchYoutubeCaptions\nRetrieve the captions/transcript for a YouTube video.\n- Supports multiple languages via the language parameter.\n- "text" export produces timestamped plain text; "json" export produces structured transcript data.\n- Input: `{ videoUrl: string, exportType: "text" | "json", language: string }`\n- Output: `{ transcripts: { text: string, start: number }[] }`\n\n#### fetchYoutubeChannel\nRetrieve metadata and recent videos for a YouTube channel.\n- Accepts a YouTube channel URL (e.g. https://www.youtube.com/@ChannelName or /channel/ID).\n- Returns channel info and video listings as a JSON object.\n- Input: `{ channelUrl: string }`\n- Output: `object`\n\n#### fetchYoutubeComments\nRetrieve comments for a YouTube video.\n- Paginates through comments (up to 5 pages).\n- "text" export produces markdown-formatted text; "json" export produces structured comment data.\n- Input: `{ videoUrl: string, exportType: "text" | "json", limitPages: string }`\n- Output: `{ comments: { id: string, link: string, publishedDate: string, text: string, likes: number, replies: number, author: string, authorLink: string, authorImg: string }[] }`\n\n#### fetchYoutubeVideo\nRetrieve metadata for a YouTube video (title, description, stats, channel info).\n- Returns video metadata, channel info, and engagement stats.\n- Video format data is excluded from the response.\n- Input: `{ videoUrl: string }`\n- Output: `object`\n\n#### searchYoutube\nSearch for YouTube videos by keyword.\n- Supports pagination (up to 5 pages) and country/language filters.\n- Use the filter/filterType fields for YouTube search parameter (sp) filters.\n- Input: `{ query: string, limitPages: string, filter: string, filterType: string, countryCode?: string, languageCode?: string }`\n- Output: `{ results: object }`\n\n#### searchYoutubeTrends\nRetrieve trending videos on YouTube by category and region.\n- Categories: "now" (trending now), "music", "gaming", "films".\n- Supports country and language filtering.\n- Input: `{ bp: "now" | "music" | "gaming" | "films", hl: string, gl: string }`\n- Output: `object`\n\n### Helpers\n\n#### `listModels()`\nList all available AI models across all categories.\n\nOutput:\n```typescript\n{\n models: {\n id: string;\n name: string; // Display name\n type: "llm_chat" | "image_generation" | "video_generation" | "video_analysis" | "text_to_speech" | "vision" | "transcription";\n maxTemperature: number;\n maxResponseSize: number;\n inputs: object[]; // Accepted input types\n }[]\n}\n```\n\n#### `listModelsByType(modelType)`\nList AI models filtered by type.\n- `modelType`: `"llm_chat"` | `"image_generation"` | `"video_generation"` | `"video_analysis"` | `"text_to_speech"` | `"vision"` | `"transcription"`\n- Output: same as `listModels()`\n\n#### `listModelsSummary()`\nList all available AI models (summary). Returns only id, name, type, and tags. Suitable for display or consumption inside a model context window.\n\nOutput:\n```typescript\n{\n models: {\n id: string;\n name: string;\n type: "llm_chat" | "image_generation" | "video_generation" | "video_analysis" | "text_to_speech" | "vision" | "transcription";\n tags: string; // Comma-separated tags\n }[]\n}\n```\n\n#### `listModelsSummaryByType(modelType)`\nList AI models (summary) filtered by type.\n- `modelType`: `"llm_chat"` | `"image_generation"` | `"video_generation"` | `"video_analysis"` | `"text_to_speech"` | `"vision"` | `"transcription"`\n- Output: same as `listModelsSummary()`\n\n#### `listConnectors()`\nList available OAuth connector services (Slack, Google, HubSpot, etc.) and their actions. These are third-party integrations \u2014 for most tasks, use actions directly instead.\n\nOutput:\n```typescript\n{\n services: {\n id: string;\n name: string;\n icon: string;\n actions: { id: string; name: string }[];\n }[]\n}\n```\n\n#### `getConnector(serviceId)`\nGet details for a single OAuth connector service by ID.\n\nOutput:\n```typescript\n{\n service: {\n id: string;\n name: string;\n icon: string;\n actions: { id: string; name: string }[];\n }\n}\n```\n\n#### `getConnectorAction(serviceId, actionId)`\nGet the full configuration for an OAuth connector action, including all input fields needed to call it via `runFromConnectorRegistry`. OAuth connectors are sourced from the open-source MindStudio Connector Registry (MSCR) with 850+ actions across third-party services.\n\nOutput:\n```typescript\n{\n action: {\n id: string;\n name: string;\n description: string;\n quickHelp: string;\n configuration: { title: string; items: { label: string; helpText: string; variable: string; type: string; defaultValue: string; placeholder: string; selectOptions?: object }[] }[];\n }\n}\n```\n\n#### `listConnections()`\nList OAuth connections for the organization (authenticated third-party service links). Use the returned connection IDs when calling OAuth connector actions. Connectors require the user to connect to the third-party service in MindStudio before they can be used.\n\nOutput:\n```typescript\n{\n connections: {\n id: string; // Connection ID to pass to connector actions\n provider: string; // Integration provider (e.g. slack, google)\n name: string; // Display name or account identifier\n }[]\n}\n```\n\n#### `estimateStepCost(stepType, step?, options?)`\nEstimate the cost of executing a step before running it. Pass the same step config you would use for execution.\n\n```typescript\nconst estimate = await mindstudio.estimateStepCost(\'generateText\', { message: \'Hello\' });\n```\n\n- `stepType`: string \u2014 The action name (e.g. `"generateText"`).\n- `step`: object \u2014 Optional action input parameters for more accurate estimates.\n- `options`: `{ appId?: string, workflowId?: string }` \u2014 Optional context for pricing.\n\nOutput:\n```typescript\n{\n costType?: string; // "free" when the step has no cost\n estimates?: {\n eventType: string; // Billing event type\n label: string; // Human-readable cost label\n unitPrice: number; // Price per unit in billing units\n unitType: string; // What constitutes a unit (e.g. "token", "request")\n estimatedCost?: number; // Estimated total cost, or null if not estimable\n quantity: number; // Number of billable units\n }[]\n}\n```\n\n#### `changeName(displayName)`\nUpdate the display name of the authenticated agent. Useful for agents to set their own name after connecting.\n\n```typescript\nawait mindstudio.changeName(\'My Agent\');\n```\n\n#### `changeProfilePicture(profilePictureUrl)`\nUpdate the profile picture of the authenticated agent. Useful for agents to set their own avatar after connecting.\n\n```typescript\nawait mindstudio.changeProfilePicture(\'https://example.com/avatar.png\');\n```\n\n#### `uploadFile(content, options)`\nUpload a file to the MindStudio CDN. Gets a signed upload URL, PUTs the file content, and returns the permanent public URL.\n\n```typescript\nimport { readFileSync } from \'fs\';\nconst { url } = await mindstudio.uploadFile(readFileSync(\'photo.png\'), { extension: \'png\', type: \'image/png\' });\n```\n\n- `content`: `Buffer | Uint8Array` \u2014 The file content.\n- `options.extension`: string \u2014 File extension without the dot (e.g. `"png"`, `"jpg"`, `"mp4"`).\n- `options.type`: string (optional) \u2014 MIME type (e.g. `"image/png"`). Determines which CDN subdomain is used.\n\nOutput: `{ url: string }` \u2014 The permanent public CDN URL.\n';
5625
+ llmsContent = '# @mindstudio-ai/agent\n\nTypeScript SDK, CLI, and MCP server for MindStudio. One API key gives you access to 200+ AI models (OpenAI, Anthropic, Google, Meta, xAI, DeepSeek, etc.) and 1,000+ actions including 850+ connector actions across third-party services from the open-source MindStudio Connector Registry (https://github.com/mindstudio-ai/mscr). No separate provider API keys required.\n\nThis file is the complete API reference. No other documentation is needed to use the SDK.\n\n## Recommended workflow\n\nThere are 150+ actions available. Do NOT try to read or load them all at once. Follow this discovery flow:\n\n1. **Identify yourself** \u2014 Call `changeName` to set your display name (use your name or whatever your user calls you). If you have a profile picture or icon, call `uploadFile` to upload it, then `changeProfilePicture` with the returned URL. This helps users identify your requests in their logs.\n2. **Ask** \u2014 Use `mindstudio ask "your question"` (CLI) or the `ask` MCP tool for SDK guidance. It knows every action, model, and connector and returns working TypeScript code with real model IDs and config options. Examples: `mindstudio ask "generate an image with FLUX"`, `mindstudio ask "what models support vision?"`, `mindstudio ask "how do I send a Slack message?"`.\n3. **Browse** \u2014 For manual discovery, call `listActions` (MCP tool) or `mindstudio list-actions --summary` (CLI) to get a compact `{ action: description }` map of everything available (~3k tokens). Call `mindstudio info <action>` (CLI) for parameter details.\n4. **Call it** \u2014 Invoke the action with the required parameters. All actions share the same calling convention (see below).\n\nFor specific use cases:\n\n- **OAuth third-party integrations** (Slack, Google, HubSpot, etc.): These are optional OAuth connectors from the MindStudio Connector Registry \u2014 for most tasks, use actions directly instead. If you need a third-party integration: call `listConnectors()` to browse services \u2192 `getConnectorAction(serviceId, actionId)` for input fields \u2192 execute via `runFromConnectorRegistry`. Requires an OAuth connection set up in MindStudio first \u2014 call `listConnections()` to check available connections.\n- **Pre-built agents**: Call `listAgents()` to see what\'s available \u2192 `runAgent({ appId })` to execute one. **Important:** Not all agents are configured for API use. Do not try to run an agent just because it appears in the list \u2014 only run agents the user specifically asks you to run.\n- **Model selection**: Call `listModelsSummary()` or `listModelsSummaryByType("llm_chat")` to browse models, then pass the model ID as `modelOverride.model` to actions like `generateText`. Use the summary endpoints (not `listModels`) to keep token usage low.\n- **Cost estimation**: AI-powered actions (text generation, image generation, video, audio, etc.) cost money. Call `estimateStepCost(stepType, stepInput)` before running these and confirm with the user before proceeding \u2014 unless they\'ve explicitly given permission to go ahead. Non-AI actions (data lookups, OAuth connectors, etc.) are generally free.\n- **Task agents**: For multi-step tasks that need autonomous tool use (research, content creation, data enrichment), use `runTask()`. Provide a prompt, input, SDK action names as tools, and a structured output example. The platform runs a tool-use loop \u2014 calling the model, executing actions, feeding results back \u2014 until structured output is produced. Example: `runTask({ prompt: "Find info about this restaurant", input: { name: "Tartine" }, tools: ["searchGoogle", "fetchUrl"], structuredOutputExample: \'{"name":"...","url":"..."}\', model: "claude-4-6-sonnet" })`.\n\n## Install\n\nStandalone binary (CLI/MCP, no dependencies):\n```bash\ncurl -fsSL https://msagent.ai/install.sh | bash\n```\n\nnpm (SDK + CLI):\n```bash\nnpm install @mindstudio-ai/agent\n```\n\nRequires Node.js >= 18.\n\n## CLI\n\nThe package includes a CLI for executing steps from the command line or scripts:\n\n```bash\n# Execute with named flags (kebab-case)\nmindstudio generate-image --prompt "A mountain landscape"\n\n# Execute with JSON input (JSON5-tolerant)\nmindstudio generate-image \'{prompt: "A mountain landscape"}\'\n\n# Extract a single output field\nmindstudio generate-image --prompt "A sunset" --output-key imageUrl\n\n# List all methods (compact JSON \u2014 best for LLM discovery)\nmindstudio list --summary\n\n# List all methods (human-readable table)\nmindstudio list\n\n# Show method details (params, types, output)\nmindstudio info generate-image\n\n# Run via npx without installing\nnpx @mindstudio-ai/agent generate-text --message "Hello"\n```\n\nAuth: run `mindstudio login`, set `MINDSTUDIO_API_KEY` env var, or pass `--api-key <key>`.\nMethod names are kebab-case on the CLI (camelCase also accepted). Flags are kebab-case (`--video-url` for `videoUrl`).\nUse `--output-key <key>` to extract a single field, `--no-meta` to strip $-prefixed metadata.\n\n### Authentication\n\n```bash\n# Interactive login (opens browser, saves key to ~/.mindstudio/config.json)\nmindstudio login\n\n# Check current auth status\nmindstudio whoami\n\n# Clear stored credentials\nmindstudio logout\n```\n\nAuth resolution order: `--api-key` flag > `MINDSTUDIO_API_KEY` env > `~/.mindstudio/config.json` > `CALLBACK_TOKEN` env.\n\n## MCP server\n\nThe package includes an MCP server exposing all methods as tools. Start by calling the `listSteps` tool to discover available methods.\n\n```bash\nmindstudio mcp\n```\n\nMCP client config (standalone binary \u2014 recommended):\n```json\n{\n "mcpServers": {\n "mindstudio": {\n "command": "mindstudio",\n "args": ["mcp"],\n "env": { "MINDSTUDIO_API_KEY": "your-api-key" }\n }\n }\n}\n```\n\n## Setup\n\nInside MindStudio apps (auth is automatic):\n```typescript\nimport { mindstudio, db, auth, Roles, stream } from \'@mindstudio-ai/agent\';\n\nconst { imageUrl } = await mindstudio.generateImage({ prompt: \'a sunset\' });\n```\n\nExternal usage (API key required):\n```typescript\nimport { MindStudioAgent } from \'@mindstudio-ai/agent\';\nconst agent = new MindStudioAgent({ apiKey: \'your-key\' });\n```\n\nYour MindStudio API key authenticates all requests. MindStudio routes to the correct AI provider (OpenAI, Google, Anthropic, etc.) server-side \u2014 you do NOT need separate provider API keys.\n\n## Models\n\nDirect access to 200+ AI models from every major provider \u2014 all through a single API key, billed at cost with no markups.\n\nUse `listModels()` or `listModelsByType()` for full model details, or `listModelsSummary()` / `listModelsSummaryByType()` for a lightweight list (id, name, type, tags) suitable for LLM context windows. Pass a model ID to `modelOverride.model` in methods like `generateText` to select a specific model:\n\n```typescript\nconst { models } = await mindstudio.listModelsByType(\'llm_chat\');\nconst model = models.find(m => m.name.includes("Gemini"));\n\nconst { content } = await mindstudio.generateText({\n message: \'Hello\',\n modelOverride: {\n model: model.id,\n temperature: 0.7,\n maxResponseTokens: 16000,\n },\n});\n```\n\n## Calling convention\n\nEvery method has the signature:\n```typescript\nmindstudio.methodName(input: InputType, options?: { appId?, threadId?, onLog? }): Promise<OutputType & StepExecutionMeta>\n```\n\nThe first argument is the step-specific input object. The optional second argument controls thread/app context and debug logging.\n\n**Debug logging**: Pass `onLog` in the options to get real-time debug logs during execution (SSE streaming). Works on ALL step methods:\n```typescript\nawait mindstudio.generateImage({ prompt: \'...\' }, {\n onLog: (event) => console.log(`[${event.tag}] ${event.value}`),\n});\n// event: { value: string, tag: string, ts: number }\n```\n\n**`stream()` \u2014 push real-time updates to the frontend:**\n```typescript\nimport { mindstudio, stream } from \'@mindstudio-ai/agent\';\n\n// String \u2192 \'token\' event (frontend receives via onToken, accumulated text)\nawait stream(\'Processing...\');\n\n// Object \u2192 \'data\' event (frontend receives via onStreamData, NOT onToken)\nawait stream({ status: \'generating\', progress: 50 });\n```\nThese are different event types \u2014 do not mix them up. `stream()` is silently ignored when no SSE connection is active (safe to include unconditionally). For long-running steps, combine `onLog` with `stream()` to push progress to the frontend.\n\n**Results are returned flat** \u2014 output fields are spread at the top level alongside metadata:\n\n```typescript\nconst { content } = await mindstudio.generateText({ message: \'Hello\' });\n\n// Full result shape for any method:\nconst result = await mindstudio.generateText({ message: `Hello` });\nresult.content; // step-specific output field\nresult.$appId; // string \u2014 app ID for this execution\nresult.$threadId; // string \u2014 thread ID for this execution\nresult.$rateLimitRemaining; // number | undefined \u2014 API calls remaining in rate limit window\nresult.$billingCost; // number | undefined \u2014 cost in credits for this call\nresult.$billingEvents; // object[] | undefined \u2014 itemized billing events\n```\n\n## Thread persistence\n\nPass `$appId`/`$threadId` from a previous result to maintain conversation state, variable state, or other context across calls:\n\n```typescript\nconst r1 = await mindstudio.generateText({ message: \'My name is Alice\' });\nconst r2 = await mindstudio.generateText(\n { message: \'What is my name?\' },\n { threadId: r1.$threadId, appId: r1.$appId },\n);\n// r2.content => "Your name is Alice"\n```\n\n## Error handling\n\nAll errors throw `MindStudioError`:\n```typescript\nimport { MindStudioError } from \'@mindstudio-ai/agent\';\n\ntry {\n await mindstudio.generateImage({ prompt: \'...\' });\n} catch (err) {\n if (err instanceof MindStudioError) {\n err.message; // Human-readable error message\n err.code; // Machine-readable code: "invalid_step_config", "api_error", "call_cap_exceeded", "output_fetch_error"\n err.status; // HTTP status code (400, 401, 429, etc.)\n err.details; // Raw error body from the API\n }\n}\n```\n\n429 rate limit errors are retried automatically (configurable via `maxRetries`).\n\n## Low-level access\n\nFor action types not covered by generated methods:\n```typescript\nconst result = await mindstudio.executeStep(\'stepType\', { ...params });\n```\n\n## Batch execution\n\nExecute multiple steps in parallel in a single request. Maximum 50 steps per batch.\nIndividual step failures do not affect other steps \u2014 partial success is possible.\n\n```typescript\nconst result = await mindstudio.executeStepBatch([\n { stepType: \'generateImage\', step: { prompt: \'a sunset\' } },\n { stepType: \'textToSpeech\', step: { text: \'hello world\' } },\n], { appId?, threadId? });\n\n// Result:\nresult.results; // BatchStepResult[] \u2014 same order as input\nresult.results[0].stepType; // string\nresult.results[0].output; // object | undefined (step output on success)\nresult.results[0].error; // string | undefined (error message on failure)\nresult.results[0].billingCost; // number | undefined (cost on success)\nresult.totalBillingCost; // number | undefined\nresult.appId; // string\nresult.threadId; // string\n```\n\nCLI:\n```bash\nmindstudio batch \'[{"stepType":"generateImage","step":{"prompt":"a cat"}}]\'\ncat steps.json | mindstudio batch\n```\n\n## Methods\n\nAll methods below are called on a `MindStudioAgent` instance (`agent.methodName(...)`).\nInput shows the first argument object. Output shows the fields available on the returned result.\n\n### General\n\n#### addSubtitlesToVideo\nAutomatically add subtitles to a video\n- Can control style of text and animation\n- Input: `{ videoUrl: string, language: string, fontName: string, fontSize: number, fontWeight: "normal" | "bold" | "black", fontColor: "white" | "black" | "red" | "green" | "blue" | "yellow" | "orange" | "purple" | "pink" | "brown" | "gray" | "cyan" | "magenta", highlightColor: "white" | "black" | "red" | "green" | "blue" | "yellow" | "orange" | "purple" | "pink" | "brown" | "gray" | "cyan" | "magenta", strokeWidth: number, strokeColor: "black" | "white" | "red" | "green" | "blue" | "yellow" | "orange" | "purple" | "pink" | "brown" | "gray" | "cyan" | "magenta", backgroundColor: "black" | "white" | "red" | "green" | "blue" | "yellow" | "orange" | "purple" | "pink" | "brown" | "gray" | "cyan" | "magenta" | "none", backgroundOpacity: number, position: "top" | "center" | "bottom", yOffset: number, wordsPerSubtitle: number, enableAnimation: boolean, intermediateAsset?: boolean }`\n- Output: `{ videoUrl: string }`\n\n#### analyzeImage\nAnalyze an image using a vision model based on a text prompt.\n- Uses the configured vision model to generate a text analysis of the image.\n- The prompt should describe what to look for or extract from the image.\n- Input: `{ prompt: string, imageUrl: string, visionModelOverride?: { model: string, config?: object } | { model: string, temperature: number, maxResponseTokens: number, ignorePreamble?: boolean, userMessagePreprocessor?: { dataSource?: string, messageTemplate?: string, maxResults?: number, enabled?: boolean, shouldInherit?: boolean }, preamble?: string, multiModelEnabled?: boolean, editResponseEnabled?: boolean, config?: object } }`\n- Output: `{ analysis: string }`\n\n#### analyzeVideo\nAnalyze a video using a video analysis model based on a text prompt.\n- Uses the configured video analysis model to generate a text analysis of the video.\n- The prompt should describe what to look for or extract from the video.\n- Input: `{ prompt: string, videoUrl: string, videoAnalysisModelOverride?: { model: string, config?: object } | { model: string, temperature: number, maxResponseTokens: number, ignorePreamble?: boolean, userMessagePreprocessor?: { dataSource?: string, messageTemplate?: string, maxResults?: number, enabled?: boolean, shouldInherit?: boolean }, preamble?: string, multiModelEnabled?: boolean, editResponseEnabled?: boolean, config?: object } }`\n- Output: `{ analysis: string }`\n\n#### captureThumbnail\nCapture a thumbnail from a video at a specified timestamp\n- Input: `{ videoUrl: string, at: number | string }`\n- Output: `{ thumbnailUrl: string }`\n\n#### checkAppRole\nCheck whether the current user has a specific app role and branch accordingly.\n- Checks if the current user has been assigned a specific role in this app.\n- If the user has the role, transitions to the "has role" path.\n- If the user does not have the role, transitions to the "no role" path, or errors if no path is configured.\n- Role names are defined by the app creator and assigned to users via the app roles system.\n- The roleName field supports {{variables}} for dynamic role checks.\n- Input: `{ roleName: string, hasRoleStepId?: string, hasRoleWorkflowId?: string, noRoleStepId?: string, noRoleWorkflowId?: string }`\n- Output: `{ hasRole: boolean, userRoles: string[] }`\n\n#### convertPdfToImages\nConvert each page of a PDF document into a PNG image.\n- Each page is converted to a separate PNG and re-hosted on the CDN.\n- Returns an array of image URLs, one per page.\n- Input: `{ pdfUrl: string }`\n- Output: `{ imageUrls: string[] }`\n\n#### createDataSource\nCreate a new empty vector data source for the current app.\n- Creates a new data source (vector database) associated with the current app version.\n- The data source is created empty \u2014 use the "Upload Data Source Document" block to add documents.\n- Returns the new data source ID which can be used in subsequent blocks.\n- Input: `{ name: string }`\n- Output: `unknown`\n\n#### createGmailDraft\nCreate a draft email in the connected Gmail account.\n- Requires a Google OAuth connection with Gmail compose scope.\n- The draft appears in the user\'s Gmail Drafts folder but is not sent.\n- messageType controls the body format: "plain" for plain text, "html" for raw HTML, "markdown" for auto-converted markdown.\n- Input: `{ to: string, subject: string, message: string, connectionId?: string, messageType: "plain" | "html" | "markdown" }`\n- Output: `{ draftId: string }`\n\n#### deleteDataSource\nDelete a vector data source from the current app.\n- Soft-deletes a data source (vector database) by marking it as deleted.\n- The Milvus partition is cleaned up asynchronously by a background cron job.\n- The data source must belong to the current app version.\n- Input: `{ dataSourceId: string }`\n- Output: `unknown`\n\n#### deleteDataSourceDocument\nDelete a single document from a data source.\n- Soft-deletes a document by marking it as deleted.\n- Requires both the data source ID and document ID.\n- After deletion, reloads vectors into Milvus so the data source reflects the change immediately.\n- Input: `{ dataSourceId: string, documentId: string }`\n- Output: `unknown`\n\n#### detectChanges\nDetect changes between runs by comparing current input against previously stored state. Routes execution based on whether a change occurred.\n- Persists state across runs using a global variable keyed to the step ID.\n- Two modes: "comparison" (default) uses strict string inequality; "ai" uses an LLM to determine if a meaningful change occurred.\n- First run always treats the value as "changed" since there is no previous state.\n- Each mode supports transitions to different steps/workflows for the "changed" and "unchanged" paths.\n- AI mode bills normally for the LLM call.\n- Input: `{ mode: "ai" | "comparison", input: string, prompt?: string, modelOverride?: { model: string, temperature: number, maxResponseTokens: number, ignorePreamble?: boolean, userMessagePreprocessor?: { dataSource?: string, messageTemplate?: string, maxResults?: number, enabled?: boolean, shouldInherit?: boolean }, preamble?: string, multiModelEnabled?: boolean, editResponseEnabled?: boolean, config?: object }, previousValueVariable?: string, changedStepId?: string, changedWorkflowId?: string, unchangedStepId?: string, unchangedWorkflowId?: string }`\n- Output: `{ hasChanged: boolean, currentValue: string, previousValue: string, isFirstRun: boolean }`\n\n#### detectPII\nScan text for personally identifiable information using Microsoft Presidio.\n- In workflow mode, transitions to detectedStepId if PII is found, notDetectedStepId otherwise.\n- In direct execution, returns the detection results without transitioning.\n- If entities is empty, returns immediately with no detections.\n- Input: `{ input: string, language: string, entities: string[], detectedStepId?: string, notDetectedStepId?: string, outputLogVariable?: string | null }`\n- Output: `{ detected: boolean, detections: { entity_type: string, start: number, end: number, score: number }[] }`\n\n#### discordEditMessage\nEdit a previously sent Discord channel message. Use with the message ID returned by Send Discord Message.\n- Only messages sent by the bot can be edited.\n- The messageId is returned by the Send Discord Message step.\n- Optionally attach a file by providing a URL to attachmentUrl. The file is downloaded and uploaded to Discord.\n- When editing with an attachment, the new attachment replaces any previous attachments on the message.\n- URLs in the text are automatically embedded by Discord (link previews for images, videos, etc.).\n- Input: `{ botToken: string, channelId: string, messageId: string, text: string, attachmentUrl?: string }`\n- Output: `unknown`\n\n#### discordSendFollowUp\nSend a follow-up message to a Discord slash command interaction.\n- Requires the applicationId and interactionToken from the Discord trigger variables.\n- Follow-up messages appear as new messages in the channel after the initial response.\n- Returns the sent message ID.\n- Interaction tokens expire after 15 minutes.\n- Optionally attach a file by providing a URL to attachmentUrl. The file is downloaded and uploaded to Discord.\n- URLs in the text are automatically embedded by Discord (link previews for images, videos, etc.).\n- Input: `{ applicationId: string, interactionToken: string, text: string, attachmentUrl?: string }`\n- Output: `{ messageId: string }`\n\n#### discordSendMessage\nSend a message to Discord \u2014 either edit the loading message or send a new channel message.\n- mode "edit" replaces the loading message (interaction response) with the final result. Uses applicationId and interactionToken from trigger variables. No bot permissions required.\n- mode "send" sends a new message to a channel. Uses botToken and channelId from trigger variables. Returns a messageId that can be used with Edit Discord Message.\n- Optionally attach a file by providing a URL to attachmentUrl. The file is downloaded and uploaded to Discord.\n- URLs in the text are automatically embedded by Discord (link previews for images, videos, etc.).\n- Interaction tokens expire after 15 minutes.\n- Input: `{ mode: "edit" | "send", text: string, applicationId?: string, interactionToken?: string, botToken?: string, channelId?: string, attachmentUrl?: string }`\n- Output: `{ messageId?: string }`\n\n#### downloadVideo\nDownload a video file\n- Works with YouTube, TikTok, etc., by using ytdlp behind the scenes\n- Can save as mp4 or mp3\n- Input: `{ videoUrl: string, format: "mp4" | "mp3" }`\n- Output: `{ videoUrl: string }`\n\n#### enhanceImageGenerationPrompt\nGenerate or enhance an image generation prompt using a language model. Optionally generates a negative prompt.\n- Rewrites the user\'s prompt with added detail about style, lighting, colors, and composition.\n- Also useful for initial generation, it doesn\'t always need to be enhancing an existing prompt\n- When includeNegativePrompt is true, a second model call generates a negative prompt.\n- Input: `{ initialPrompt: string, includeNegativePrompt: boolean, negativePromptDestinationVariableName?: string, systemPrompt: string, modelOverride?: unknown }`\n- Output: `{ prompt: string, negativePrompt?: string }`\n\n#### enhanceVideoGenerationPrompt\nGenerate or enhance a video generation prompt using a language model. Optionally generates a negative prompt.\n- Rewrites the user\'s prompt with added detail about style, camera movement, lighting, and composition.\n- Also useful for initial generation, it doesn\'t always need to be enhancing an existing prompt\n- When includeNegativePrompt is true, a second model call generates a negative prompt.\n- Input: `{ initialPrompt: string, includeNegativePrompt: boolean, negativePromptDestinationVariableName?: string, systemPrompt: string, modelOverride?: unknown }`\n- Output: `{ prompt: string, negativePrompt?: string }`\n\n#### extractAudioFromVideo\nExtract audio MP3 from a video file\n- Input: `{ videoUrl: string }`\n- Output: `{ audioUrl: string }`\n\n#### extractText\nDownload a file from a URL and extract its text content. Supports PDFs (including scanned/image-based PDFs via OCR), plain text files, and other document formats.\n- Best suited for PDFs and raw text/document files. For web pages, use the scrapeUrl step instead.\n- Handles both text-layer PDFs and image-based/scanned PDFs (e.g. Figma/Canva exports, scanned documents). Image-based PDFs are processed with OCR automatically \u2014 there is no need to convert PDF pages to images first.\n- Accepts a single URL, a comma-separated list of URLs, or a JSON array of URLs.\n- Files are rehosted on the MindStudio CDN before extraction.\n- Maximum file size is 50MB per URL.\n- Input: `{ url: string | string[] }`\n- Output: `{ text: string | string[] }`\n\n#### fetchDataSourceDocument\nFetch the full extracted text contents of a document in a data source.\n- Loads a document by ID and returns its full extracted text content.\n- The document must have been successfully processed (status "done").\n- Also returns document metadata (name, summary, word count).\n- Input: `{ dataSourceId: string, documentId: string }`\n- Output: `unknown`\n\n#### fetchSlackChannelHistory\nFetch recent message history from a Slack channel.\n- The user is responsible for connecting their Slack workspace and selecting the channel\n- Input: `{ connectionId?: string, channelId: string, limit?: number, startDate?: string, endDate?: string, includeImages?: boolean, includeRawMessage?: boolean }`\n- Output: `{ messages: { from: string, content: string, timestamp?: string, images?: string[], rawMessage?: { app_id?: string, assistant_app_thread?: { first_user_thread_reply?: string, title?: string, title_blocks?: unknown[] }, attachments?: { actions?: unknown[], app_id?: string, app_unfurl_url?: string, author_icon?: string, author_id?: string, author_link?: string, author_name?: string, author_subname?: string, blocks?: unknown[], bot_id?: string, bot_team_id?: string, callback_id?: string, channel_id?: string, channel_name?: string, channel_team?: string, color?: string, fallback?: string, fields?: unknown[], file_id?: string, filename?: string, files?: unknown[], footer?: string, footer_icon?: string, from_url?: string, hide_border?: boolean, hide_color?: boolean, id?: number, image_bytes?: number, image_height?: number, image_url?: string, image_width?: number, indent?: boolean, is_app_unfurl?: boolean, is_file_attachment?: boolean, is_msg_unfurl?: boolean, is_reply_unfurl?: boolean, is_thread_root_unfurl?: boolean, list?: unknown, list_record?: unknown, list_record_id?: string, list_records?: unknown[], list_schema?: unknown[], list_view?: unknown, list_view_id?: string, message_blocks?: unknown[], metadata?: unknown, mimetype?: string, mrkdwn_in?: string[], msg_subtype?: string, original_url?: string, pretext?: string, preview?: unknown, service_icon?: string, service_name?: string, service_url?: string, size?: number, text?: string, thumb_height?: number, thumb_url?: string, thumb_width?: number, title?: string, title_link?: string, ts?: string, url?: string, video_html?: string, video_html_height?: number, video_html_width?: number, video_url?: string }[], blocks?: { accessory?: unknown, alt_text?: string, api_decoration_available?: boolean, app_collaborators?: string[], app_id?: string, author_name?: string, block_id?: string, bot_user_id?: string, button_label?: string, call?: unknown, call_id?: string, description?: unknown, developer_trace_id?: string, dispatch_action?: boolean, element?: unknown, elements?: unknown[], expand?: boolean, external_id?: string, fallback?: string, fields?: unknown[], file?: unknown, file_id?: string, function_trigger_id?: string, hint?: unknown, image_bytes?: number, image_height?: number, image_url?: string, image_width?: number, is_animated?: boolean, is_workflow_app?: boolean, label?: unknown, optional?: boolean, owning_team_id?: string, provider_icon_url?: string, provider_name?: string, sales_home_workflow_app_type?: number, share_url?: string, slack_file?: unknown, source?: string, text?: unknown, thumbnail_url?: string, title?: unknown, title_url?: string, trigger_subtype?: string, trigger_type?: string, type?: unknown, url?: string, video_url?: string, workflow_id?: string }[], bot_id?: string, bot_profile?: { app_id?: string, deleted?: boolean, icons?: unknown, id?: string, name?: string, team_id?: string, updated?: number }, client_msg_id?: string, display_as_bot?: boolean, edited?: { ts?: string, user?: string }, files?: { access?: string, alt_txt?: string, app_id?: string, app_name?: string, attachments?: unknown[], blocks?: unknown[], bot_id?: string, can_toggle_canvas_lock?: boolean, canvas_printing_enabled?: boolean, canvas_template_mode?: string, cc?: unknown[], channel_actions_count?: number, channel_actions_ts?: string, channels?: string[], comments_count?: number, converted_pdf?: string, created?: number, deanimate?: string, deanimate_gif?: string, display_as_bot?: boolean, dm_mpdm_users_with_file_access?: unknown[], duration_ms?: number, edit_link?: string, edit_timestamp?: number, editable?: boolean, editor?: string, editors?: string[], external_id?: string, external_type?: string, external_url?: string, favorites?: unknown[], file_access?: string, filetype?: string, from?: unknown[], groups?: string[], has_more?: boolean, has_more_shares?: boolean, has_rich_preview?: boolean, headers?: unknown, hls?: string, hls_embed?: string, id?: string, image_exif_rotation?: number, ims?: string[], initial_comment?: unknown, is_channel_space?: boolean, is_external?: boolean, is_public?: boolean, is_restricted_sharing_enabled?: boolean, is_starred?: boolean, last_editor?: string, last_read?: number, lines?: number, lines_more?: number, linked_channel_id?: string, list_csv_download_url?: string, list_limits?: unknown, list_metadata?: unknown, media_display_type?: string, media_progress?: unknown, mimetype?: string, mode?: string, mp4?: string, mp4_low?: string, name?: string, non_owner_editable?: boolean, num_stars?: number, org_or_workspace_access?: string, original_attachment_count?: number, original_h?: string, original_w?: string, permalink?: string, permalink_public?: string, pinned_to?: string[], pjpeg?: string, plain_text?: string, pretty_type?: string, preview?: string, preview_highlight?: string, preview_is_truncated?: boolean, preview_plain_text?: string, private_channels_with_file_access_count?: number, private_file_with_access_count?: number, public_url_shared?: boolean, quip_thread_id?: string, reactions?: unknown[], saved?: unknown, sent_to_self?: boolean, shares?: unknown, show_badge?: boolean, simplified_html?: string, size?: number, source_team?: string, subject?: string, subtype?: string, team_pref_version_history_enabled?: boolean, teams_shared_with?: unknown[], template_conversion_ts?: number, template_description?: string, template_icon?: string, template_name?: string, template_title?: string, thumb_1024?: string, thumb_1024_gif?: string, thumb_1024_h?: string, thumb_1024_w?: string, thumb_160?: string, thumb_160_gif?: string, thumb_160_h?: string, thumb_160_w?: string, thumb_360?: string, thumb_360_gif?: string, thumb_360_h?: string, thumb_360_w?: string, thumb_480?: string, thumb_480_gif?: string, thumb_480_h?: string, thumb_480_w?: string, thumb_64?: string, thumb_64_gif?: string, thumb_64_h?: string, thumb_64_w?: string, thumb_720?: string, thumb_720_gif?: string, thumb_720_h?: string, thumb_720_w?: string, thumb_80?: string, thumb_800?: string, thumb_800_gif?: string, thumb_800_h?: string, thumb_800_w?: string, thumb_80_gif?: string, thumb_80_h?: string, thumb_80_w?: string, thumb_960?: string, thumb_960_gif?: string, thumb_960_h?: string, thumb_960_w?: string, thumb_gif?: string, thumb_pdf?: string, thumb_pdf_h?: string, thumb_pdf_w?: string, thumb_tiny?: string, thumb_video?: string, thumb_video_h?: number, thumb_video_w?: number, timestamp?: number, title?: string, title_blocks?: unknown[], to?: unknown[], transcription?: unknown, update_notification?: number, updated?: number, url_private?: string, url_private_download?: string, url_static_preview?: string, user?: string, user_team?: string, username?: string, vtt?: string }[], icons?: { emoji?: string, image_36?: string, image_48?: string, image_64?: string, image_72?: string }, inviter?: string, is_locked?: boolean, latest_reply?: string, metadata?: { event_payload?: unknown, event_type?: string }, parent_user_id?: string, purpose?: string, reactions?: { count?: number, name?: string, url?: string, users?: string[] }[], reply_count?: number, reply_users?: string[], reply_users_count?: number, root?: { bot_id?: string, icons?: unknown, latest_reply?: string, parent_user_id?: string, reply_count?: number, reply_users?: string[], reply_users_count?: number, subscribed?: boolean, subtype?: string, text?: string, thread_ts?: string, ts?: string, type?: string, username?: string }, subscribed?: boolean, subtype?: string, team?: string, text?: string, thread_ts?: string, topic?: string, ts?: string, type?: string, upload?: boolean, user?: string, username?: string, x_files?: string[] } }[] }`\n\n#### generateAsset\nGenerate an HTML asset and export it as a webpage, PDF, or image\n- Agents can generate HTML documents and export as webpage, PDFs, images, or videos. They do this by using the "generatePdf" block, which defines an HTML page with variables, and then the generation process renders the page to create the output and save its URL at the specified variable.\n- The template for the HTML page is generated by a separate process, and it can only use variables that have already been defined in the workflow at the time of its execution. It has full access to handlebars to render the HTML template, including a handlebars helper to render a markdown variable string as HTML (which can be useful for creating templates that render long strings). The template can also create its own simple JavaScript to do things like format dates and strings.\n- If PDF or composited image generation are part of the workflow, assistant adds the block and leaves the "source" empty. In a separate step, assistant generates a detailed request for the developer who will write the HTML.\n- Can also auto-generate HTML from a prompt (like a generate text block to generate HTML). In these cases, create a prompt with variables in the dynamicPrompt variable describing, in detail, the document to generate\n- Can either display output directly to user (foreground mode) or save the URL of the asset to a variable (background mode)\n- Input: `{ source: string, sourceType: "html" | "markdown" | "spa" | "raw" | "dynamic" | "customInterface", outputFormat: "pdf" | "png" | "html" | "mp4" | "openGraph", pageSize: "full" | "letter" | "A4" | "custom", testData: object, options?: { pageWidthPx?: number, pageHeightPx?: number, pageOrientation?: "portrait" | "landscape", rehostMedia?: boolean, videoDurationSeconds?: number }, spaSource?: { source?: string, lastCompiledSource?: string, files?: object, paths: string[], root: string, zipUrl: string }, rawSource?: string, dynamicPrompt?: string, dynamicSourceModelOverride?: { model: string, temperature: number, maxResponseTokens: number, ignorePreamble?: boolean, userMessagePreprocessor?: { dataSource?: string, messageTemplate?: string, maxResults?: number, enabled?: boolean, shouldInherit?: boolean }, preamble?: string, multiModelEnabled?: boolean, editResponseEnabled?: boolean, config?: object }, transitionControl?: "default" | "native", shareControl?: "default" | "hidden", shareImageUrl?: string, intermediateAsset?: boolean }`\n- Output: `{ url: string }`\n\n#### generateChart\nCreate a chart image using QuickChart (Chart.js) and return the URL.\n- The data field must be a Chart.js-compatible JSON object serialized as a string.\n- Supported chart types: bar, line, pie.\n- Input: `{ chart: { chartType: "bar" | "line" | "pie", data: string, options: { width: string, height: string } } }`\n- Output: `{ chartUrl: string }`\n\n#### generateImage\nGenerate an image from a text prompt using an AI model.\n- Prompts should be descriptive but concise (roughly 3\u20136 sentences).\n- Images are automatically hosted on a CDN.\n- In foreground mode, the image is displayed to the user. In background mode, the URL is saved to a variable.\n- When generateVariants is true with numVariants > 1, multiple images are generated in parallel.\n- In direct execution, foreground mode behaves as background, and userSelect variant behavior behaves as saveAll.\n- Input: `{ prompt: string, intermediateAsset?: boolean, imageModelOverride?: { model: string, config?: object }, generateVariants?: boolean, numVariants?: number, addWatermark?: boolean }`\n- Output: `{ imageUrl: string | string[] }`\n\n#### generateLipsync\nGenerate a lip sync video from provided audio and image.\n- In foreground mode, the video is displayed to the user. In background mode, the URL is saved to a variable.\n- Input: `{ intermediateAsset?: boolean, addWatermark?: boolean, lipsyncModelOverride?: { model: string, config?: object } }`\n- Output: `unknown`\n\n#### generateMusic\nGenerate an audio file from provided instructions (text) using a music model.\n- The text field contains the instructions (prompt) for the music generation.\n- In foreground mode, the audio is displayed to the user. In background mode, the URL is saved to a variable.\n- Input: `{ text: string, intermediateAsset?: boolean, musicModelOverride?: { model: string, config?: object } }`\n- Output: `unknown`\n\n#### generateStaticVideoFromImage\nConvert a static image to an MP4\n- Can use to create slides/intertitles/slates for video composition\n- Input: `{ imageUrl: string, duration: string }`\n- Output: `{ videoUrl: string }`\n\n#### generateText\nSend a message to an AI model and return the response, or echo a system message.\n- Source "user" sends the message to an LLM and returns the model\'s response.\n- Source "system" echoes the message content directly (no AI call).\n- Mode "background" saves the result to a variable. Mode "foreground" streams it to the user (not available in direct execution).\n- Structured output (JSON/CSV) can be enforced via structuredOutputType and structuredOutputExample.\n- When executed inside a v2 app method (managed sandbox or local dev tunnel),\nLLM token output can be streamed to the frontend in real time via an SSE\nside-channel. The frontend opts in by passing { stream: true } to the method\ninvocation via @mindstudio-ai/interface. Tokens are published to Redis\npub/sub as they arrive and forwarded as SSE events on the invoke response.\nThe method code itself is unchanged \u2014 streaming is transparent to the\ndeveloper. See V2ExecutionService.ts and the invoke handler in V2Apps for\nthe server-side plumbing.\n- Input: `{ message: string, source?: "user" | "system", modelOverride?: { model: string, temperature: number, maxResponseTokens: number, ignorePreamble?: boolean, userMessagePreprocessor?: { dataSource?: string, messageTemplate?: string, maxResults?: number, enabled?: boolean, shouldInherit?: boolean }, preamble?: string, multiModelEnabled?: boolean, editResponseEnabled?: boolean, config?: object }, structuredOutputType?: "text" | "json" | "csv", structuredOutputExample?: string, chatHistoryMode?: "include" | "exclude" }`\n- Output: `{ content: string }`\n\n#### generateVideo\nGenerate a video from a text prompt using an AI model.\n- Prompts should be descriptive but concise (roughly 3\u20136 sentences).\n- Videos are automatically hosted on a CDN.\n- In foreground mode, the video is displayed to the user. In background mode, the URL is saved to a variable.\n- When generateVariants is true with numVariants > 1, multiple videos are generated in parallel.\n- In direct execution, foreground mode behaves as background, and userSelect variant behavior behaves as saveAll.\n- Input: `{ prompt: string, intermediateAsset?: boolean, videoModelOverride?: { model: string, config?: object }, generateVariants?: boolean, numVariants?: number, addWatermark?: boolean }`\n- Output: `{ videoUrl: string | string[] }`\n\n#### getGmailAttachments\nDownload attachments from a Gmail email and re-host them on CDN.\n- Requires a Google OAuth connection with Gmail readonly scope.\n- Attachments are uploaded to CDN and returned as URLs.\n- Attachments larger than 25MB are skipped.\n- Use the message ID from Search Gmail Emails, List Recent Gmail Emails, or Get Gmail Email steps.\n- Input: `{ messageId: string, connectionId?: string }`\n- Output: `unknown`\n\n#### getGmailUnreadCount\nGet the number of unread emails in the connected Gmail inbox.\n- Requires a Google OAuth connection with Gmail readonly scope.\n- Returns the unread message count for the inbox label.\n- This is a lightweight call that does not fetch any email content.\n- Input: `{ connectionId?: string }`\n- Output: `unknown`\n\n#### getMediaMetadata\nGet info about a media file\n- Input: `{ mediaUrl: string }`\n- Output: `{ metadata: string }`\n\n#### imageFaceSwap\nReplace a face in an image with a face from another image using AI.\n- Requires both a target image and a face source image.\n- Output is re-hosted on the CDN as a PNG.\n- Input: `{ imageUrl: string, faceImageUrl: string, engine: string }`\n- Output: `{ imageUrl: string }`\n\n#### imageRemoveWatermark\nRemove watermarks from an image using AI.\n- Output is re-hosted on the CDN as a PNG.\n- Input: `{ imageUrl: string, engine: string, intermediateAsset?: boolean }`\n- Output: `{ imageUrl: string }`\n\n#### insertVideoClips\nInsert b-roll clips into a base video at a timecode, optionally with an xfade transition.\n- Input: `{ baseVideoUrl: string, overlayVideos: { videoUrl: string, startTimeSec: number }[], transition?: string, transitionDuration?: number, useOverlayAudio?: boolean, intermediateAsset?: boolean }`\n- Output: `{ videoUrl: string }`\n\n#### listDataSources\nList all data sources for the current app.\n- Returns metadata for every data source associated with the current app version.\n- Each entry includes the data source ID, name, description, status, and document list.\n- Input: `object`\n- Output: `unknown`\n\n#### listGmailLabels\nList all labels in the connected Gmail account. Use these label IDs or names with the Update Gmail Labels step.\n- Requires a Google OAuth connection with Gmail readonly scope.\n- Returns both system labels (INBOX, SENT, TRASH, etc.) and user-created labels.\n- Label type is "system" for built-in labels or "user" for custom labels.\n- Input: `{ connectionId?: string }`\n- Output: `unknown`\n\n#### listRecentGmailEmails\nList recent emails from the connected Gmail inbox.\n- Requires a Google OAuth connection with Gmail readonly scope.\n- Returns up to 100 emails (default 5), ordered by most recent first.\n- Functionally equivalent to Search Gmail Emails with an "in:inbox" query.\n- Input: `{ connectionId?: string, exportType: "json" | "text", limit: string }`\n- Output: `unknown`\n\n#### logic\nRoute execution to different branches based on AI evaluation, comparison operators, or workflow jumps.\n- Supports two modes: "ai" (default) uses an AI model to pick the most accurate statement; "comparison" uses operator-based checks.\n- In AI mode, the model picks the most accurate statement from the list. All possible cases must be specified.\n- In comparison mode, the context is the left operand and each case\'s condition is the right operand. First matching case wins. Use operator "default" as a fallback.\n- Requires at least two cases.\n- Each case can transition to a step in the current workflow (destinationStepId) or jump to another workflow (destinationWorkflowId).\n- Input: `{ mode?: "ai" | "comparison", context: string, cases: ({ id: string, condition: string, operator?: "eq" | "neq" | "gt" | "lt" | "gte" | "lte" | "exists" | "not_exists" | "contains" | "not_contains" | "default", destinationStepId?: string, destinationWorkflowId?: string } | string)[], modelOverride?: { model: string, temperature: number, maxResponseTokens: number, ignorePreamble?: boolean, userMessagePreprocessor?: { dataSource?: string, messageTemplate?: string, maxResults?: number, enabled?: boolean, shouldInherit?: boolean }, preamble?: string, multiModelEnabled?: boolean, editResponseEnabled?: boolean, config?: object } }`\n- Output: `{ selectedCase: number }`\n\n#### makeDotComRunScenario\nTrigger a Make.com (formerly Integromat) scenario via webhook and return the response.\n- The webhook URL must be configured in your Make.com scenario.\n- Input key-value pairs are sent as JSON in the POST body.\n- Response format depends on the Make.com scenario configuration.\n- Input: `{ webhookUrl: string, input: object }`\n- Output: `{ data: unknown }`\n\n#### mergeAudio\nMerge one or more clips into a single audio file.\n- Input: `{ mp3Urls: string[], fileMetadata?: object, albumArtUrl?: string, intermediateAsset?: boolean }`\n- Output: `{ audioUrl: string }`\n\n#### mergeVideos\nMerge one or more clips into a single video.\n- Input: `{ videoUrls: string[], transition?: string, transitionDuration?: number, intermediateAsset?: boolean }`\n- Output: `{ videoUrl: string }`\n\n#### mixAudioIntoVideo\nMix an audio track into a video\n- Input: `{ videoUrl: string, audioUrl: string, options: { keepVideoAudio?: boolean, audioGainDb?: number, videoGainDb?: number, loopAudio?: boolean }, intermediateAsset?: boolean }`\n- Output: `{ videoUrl: string }`\n\n#### muteVideo\nMute a video file\n- Input: `{ videoUrl: string, intermediateAsset?: boolean }`\n- Output: `{ videoUrl: string }`\n\n#### n8nRunNode\nTrigger an n8n workflow node via webhook and return the response.\n- The webhook URL must be configured in your n8n workflow.\n- Supports GET and POST methods with optional Basic authentication.\n- For GET requests, input values are sent as query parameters. For POST, they are sent as JSON body.\n- Input: `{ method: string, authentication: "none" | "basic" | "string", user: string, password: string, webhookUrl: string, input: object }`\n- Output: `{ data: unknown }`\n\n#### postToSlackChannel\nSend a message to a Slack channel via a connected bot.\n- The user is responsible for connecting their Slack workspace and selecting the channel\n- Supports both simple text messages and slack blocks messages\n- Text messages can use limited markdown (slack-only fomatting\u2014e.g., headers are just rendered as bold)\n- Input: `{ channelId: string, messageType: "string" | "blocks", message: string, connectionId?: string }`\n- Output: `unknown`\n\n#### postToZapier\nSend data to a Zapier Zap via webhook and return the response.\n- The webhook URL must be configured in the Zapier Zap settings\n- Input keys and values are sent as the JSON body of the POST request\n- The webhook response (JSON or plain text) is returned as the output\n- Input: `{ webhookUrl: string, input: object }`\n- Output: `{ data: unknown }`\n\n#### queryAppDatabase\nExecute a SQL query against the app managed database.\n- Executes raw SQL against a SQLite database managed by the app.\n- For SELECT queries, returns rows as JSON.\n- For INSERT/UPDATE/DELETE, returns the number of affected rows.\n- Use {{variables}} directly in your SQL. By default they are automatically extracted\nand passed as safe parameterized values (preventing SQL injection).\nExample: INSERT INTO contacts (name, comment) VALUES ({{name}}, {{comment}})\n- Full MindStudio handlebars syntax is supported, including helpers like {{json myVar}},\n{{get myVar "$.path"}}, {{global.orgName}}, etc.\n- Set parameterize to false for raw/dynamic SQL where variables are interpolated directly\ninto the query string. Use this when another step generates full or partial SQL, e.g.\na bulk INSERT with a precomputed VALUES list. The user is responsible for sanitization\nwhen parameterize is false.\n- Input: `{ databaseId: string, sql: string, parameterize?: boolean }`\n- Output: `{ rows: unknown[], changes: number }`\n\n#### queryDataSource\nSearch a vector data source (RAG) and return relevant document chunks.\n- Queries a vectorized data source and returns the most relevant chunks.\n- Useful for retrieval-augmented generation (RAG) workflows.\n- Input: `{ dataSourceId: string, query: string, maxResults: number }`\n- Output: `{ text: string, chunks: string[], query: string, citations: unknown[], latencyMs: number }`\n\n#### queryExternalDatabase\nExecute a SQL query against an external database connected to the workspace.\n- Requires a database connection configured in the workspace.\n- Supports PostgreSQL (including Supabase), MySQL, and MSSQL.\n- Results can be returned as JSON or CSV.\n- Input: `{ connectionId?: string, query: string, outputFormat: "json" | "csv" }`\n- Output: `{ data: unknown }`\n\n#### redactPII\nReplace personally identifiable information in text with placeholders using Microsoft Presidio.\n- PII is replaced with entity type placeholders (e.g. "Call me at <PHONE_NUMBER>").\n- If entities is empty, returns empty text immediately without processing.\n- Input: `{ input: string, language: string, entities: string[] }`\n- Output: `{ text: string }`\n\n#### removeBackgroundFromImage\nRemove the background from an image using AI, producing a transparent PNG.\n- Uses the Bria background removal model via fal.ai.\n- Output is re-hosted on the CDN as a PNG with transparency.\n- Input: `{ imageUrl: string }`\n- Output: `{ imageUrl: string }`\n\n#### resizeVideo\nResize a video file\n- Input: `{ videoUrl: string, mode: "fit" | "exact", maxWidth?: number, maxHeight?: number, width?: number, height?: number, strategy?: "pad" | "crop", intermediateAsset?: boolean }`\n- Output: `{ videoUrl: string }`\n\n#### runFromConnectorRegistry\nRun a raw API connector to a third-party service\n- Use the /developer/v2/helpers/connectors endpoint to list available services and actions.\n- Use /developer/v2/helpers/connectors/{serviceId}/{actionId} to get the full input configuration for an action.\n- Use /developer/v2/helpers/connections to list your available OAuth connections.\n- The actionId format is "serviceId/actionId" (e.g., "slack/send-message").\n- Pass a __connectionId to authenticate the request with a specific OAuth connection, otherwise the default will be used (if configured).\n- Input: `{ actionId: string, displayName: string, icon: string, configurationValues: object, __connectionId?: string }`\n- Output: `{ data: object }`\n\n#### runPackagedWorkflow\nRun a packaged workflow ("custom block")\n- From the user\'s perspective, packaged workflows are just ordinary blocks. Behind the scenes, they operate like packages/libraries in a programming language, letting the user execute custom functionality.\n- Some of these packaged workflows are available as part of MindStudio\'s "Standard Library" and available to every user.\n- Available packaged workflows are documented here as individual blocks, but the runPackagedWorkflow block is how they need to be wrapped in order to be executed correctly.\n- Input: `{ appId: string, workflowId: string, inputVariables: object, outputVariables: object, name: string }`\n- Output: `{ data: unknown }`\n\n#### scrapeLinkedInCompany\nScrape public company data from a LinkedIn company page.\n- Requires a LinkedIn company URL (e.g. https://www.linkedin.com/company/mindstudioai).\n- Returns structured company data including description, employees, updates, and similar companies.\n- Input: `{ url: string }`\n- Output: `{ company: unknown }`\n\n#### scrapeLinkedInProfile\nScrape public profile data from a LinkedIn profile page.\n- Requires a LinkedIn profile URL (e.g. https://www.linkedin.com/in/username).\n- Returns structured profile data including experience, education, articles, and activities.\n- Input: `{ url: string }`\n- Output: `{ profile: unknown }`\n\n#### scrapeUrl\nExtract text, HTML, or structured content from one or more web pages.\n- Accepts a single URL or multiple URLs (as a JSON array, comma-separated, or newline-separated).\n- Output format controls the result shape: "text" returns markdown, "html" returns raw HTML, "json" returns structured scraper data.\n- Can optionally capture a screenshot of each page.\n- Input: `{ url: string, service?: "default" | "firecrawl", autoEnhance?: boolean, pageOptions?: { onlyMainContent: boolean, screenshot: boolean, waitFor: number, replaceAllPathsWithAbsolutePaths: boolean, headers: object, removeTags: string[], mobile: boolean } }`\n- Output: `{ content: string | string[] | { text: string, html: string, json?: object, screenshotUrl?: string, metadata?: { title: string, description: string, url: string, image: string } } | { text: string, html: string, json?: object, screenshotUrl?: string, metadata?: { title: string, description: string, url: string, image: string } }[], screenshot?: string }`\n\n#### scrapeXPost\nScrape data from a single X (Twitter) post by URL.\n- Returns structured post data (text, html, optional json/screenshot/metadata).\n- Optionally saves the text content to a variable.\n- Input: `{ url: string }`\n- Output: `{ post: { text: string, html: string, json?: object, screenshotUrl?: string, metadata?: { title: string, description: string, url: string, image: string } } }`\n\n#### scrapeXProfile\nScrape public profile data from an X (Twitter) account by URL.\n- Returns structured profile data.\n- Optionally saves the result to a variable.\n- Input: `{ url: string }`\n- Output: `{ profile: { text: string, html: string, json?: object, screenshotUrl?: string, metadata?: { title: string, description: string, url: string, image: string } } }`\n\n#### screenshotUrl\nCapture a screenshot of a web page as a PNG image.\n- Takes a viewport or full-page screenshot of the given URL.\n- Returns a CDN-hosted PNG image URL.\n- Viewport mode captures only the visible area; fullPage captures the entire scrollable page.\n- You can customize viewport width/height, add a delay, or wait for a CSS selector before capturing.\n- Input: `{ url: string, mode?: "viewport" | "fullPage", width?: number, height?: number, delay?: number, waitFor?: string }`\n- Output: `{ screenshotUrl: string }`\n\n#### searchGmailEmails\nSearch for emails in the connected Gmail account using a Gmail search query. To list recent inbox emails, pass an empty query string.\n- Requires a Google OAuth connection with Gmail readonly scope.\n- Uses Gmail search syntax (e.g. "from:user@example.com", "subject:invoice", "is:unread").\n- To list recent inbox emails, use an empty query string or "in:inbox".\n- Returns up to 100 emails (default 5). The variable receives text or JSON depending on exportType.\n- The direct execution output always returns structured email objects.\n- Input: `{ query: string, connectionId?: string, exportType: "json" | "text", limit: string }`\n- Output: `{ emails: { id: string, subject: string, from: string, to: string, date: string, plainBody: string, htmlBody: string, labels: string }[] }`\n\n#### searchGoogle\nSearch the web using Google and return structured results.\n- Defaults to us/english, but can optionally specify country and/or language.\n- Defaults to any time, but can optionally specify last hour, last day, week, month, or year.\n- Defaults to top 30 results, but can specify 1 to 100 results to return.\n- Input: `{ query: string, exportType: "text" | "json", countryCode?: string, languageCode?: string, dateRange?: "hour" | "day" | "week" | "month" | "year" | "any", numResults?: number }`\n- Output: `{ results: { title: string, description: string, url: string }[] }`\n\n#### searchGoogleImages\nSearch Google Images and return image results with URLs and metadata.\n- Defaults to us/english, but can optionally specify country and/or language.\n- Defaults to any time, but can optionally specify last hour, last day, week, month, or year.\n- Defaults to top 30 results, but can specify 1 to 100 results to return.\n- Input: `{ query: string, exportType: "text" | "json", countryCode?: string, languageCode?: string, dateRange?: "hour" | "day" | "week" | "month" | "year" | "any", numResults?: number }`\n- Output: `{ images: { title: string, imageUrl: string, imageWidth: number, imageHeight: number, thumbnailUrl: string, thumbnailWidth: number, thumbnailHeight: number, source: string, domain: string, link: string, googleUrl: string, position: number }[] }`\n\n#### searchGoogleNews\nSearch Google News for recent news articles matching a query.\n- Defaults to top 30 results, but can specify 1 to 100 results to return.\n- Input: `{ text: string, exportType: "text" | "json", numResults?: number }`\n- Output: `{ articles: { title: string, link: string, date: string, source: { name: string }, snippet?: string }[] }`\n\n#### searchGoogleTrends\nFetch Google Trends data for a search term.\n- date accepts shorthand ("now 1-H", "today 1-m", "today 5-y", etc.) or custom "yyyy-mm-dd yyyy-mm-dd" ranges.\n- data_type controls the shape of returned data: TIMESERIES, GEO_MAP, GEO_MAP_0, RELATED_TOPICS, or RELATED_QUERIES.\n- Input: `{ text: string, hl: string, geo: string, data_type: "TIMESERIES" | "GEO_MAP" | "GEO_MAP_0" | "RELATED_TOPICS" | "RELATED_QUERIES", cat: string, date: string, ts: string }`\n- Output: `{ trends: object }`\n\n#### searchPerplexity\nSearch the web using the Perplexity API and return structured results.\n- Defaults to US results. Use countryCode (ISO code) to filter by country.\n- Returns 10 results by default, configurable from 1 to 20.\n- The variable receives text or JSON depending on exportType. The direct execution output always returns structured results.\n- Input: `{ query: string, exportType: "text" | "json", countryCode?: string, numResults?: number }`\n- Output: `{ results: { title: string, description: string, url: string }[] }`\n\n#### sendEmail\nSend an email to one or more recipient addresses.\n- Use the "to" field to send to specific email addresses directly. For v2 apps, recipients must be verified users in the app\'s user table.\n- Alternatively, recipient email addresses can be resolved from OAuth connections configured by the app creator via connectionId. The user running the workflow does not specify the recipient directly.\n- If the body is a URL to a hosted HTML file on the CDN, the HTML is fetched and used as the email body.\n- When generateHtml is enabled, the body text is converted to a styled HTML email using an AI model.\n- connectionId can be a comma-separated list to send to multiple recipients.\n- The special connectionId "trigger_email" uses the email address that triggered the workflow.\n- Input: `{ subject: string, body: string, to?: string | string[], connectionId?: string, generateHtml?: boolean, generateHtmlInstructions?: string, generateHtmlModelOverride?: { model: string, temperature: number, maxResponseTokens: number, ignorePreamble?: boolean, userMessagePreprocessor?: { dataSource?: string, messageTemplate?: string, maxResults?: number, enabled?: boolean, shouldInherit?: boolean }, preamble?: string, multiModelEnabled?: boolean, editResponseEnabled?: boolean, config?: object }, attachments?: string[] }`\n- Output: `{ recipients: string[] }`\n\n#### sendGmailDraft\nSend an existing draft from the connected Gmail account.\n- Requires a Google OAuth connection with Gmail compose scope.\n- The draft is sent and removed from the Drafts folder.\n- Use the draft ID returned by the Create Gmail Draft or List Gmail Drafts steps.\n- Input: `{ draftId: string, connectionId?: string }`\n- Output: `unknown`\n\n#### sendGmailMessage\nSend an email from the connected Gmail account.\n- Requires a Google OAuth connection with Gmail compose scope.\n- messageType controls the body format: "plain" for plain text, "html" for raw HTML, "markdown" for auto-converted markdown.\n- Input: `{ to: string, subject: string, message: string, connectionId?: string, messageType: "plain" | "html" | "markdown" }`\n- Output: `{ messageId: string }`\n\n#### sendSlackDirectMessage\nSend a direct message to a Slack user via a connected bot.\n- The user is responsible for connecting their Slack workspace\n- The recipient is identified by their Slack user ID\n- Supports both simple text messages and Slack blocks messages\n- Text messages can use limited markdown (slack-only formatting\u2014e.g., headers are just rendered as bold)\n- Input: `{ slackUserId: string, messageType: "string" | "blocks", message: string, connectionId?: string }`\n- Output: `unknown`\n\n#### sendSMS\nSend an SMS or MMS message to a phone number configured via OAuth connection.\n- User is responsible for configuring the connection to the number (MindStudio requires double opt-in to prevent spam)\n- If mediaUrls are provided, the message is sent as MMS instead of SMS\n- MMS supports up to 10 media URLs (images, video, audio, PDF) with a 5MB limit per file\n- MMS is only supported on US and Canadian carriers; international numbers will receive SMS only (media silently dropped)\n- Input: `{ body: string, connectionId?: string, mediaUrls?: string[] }`\n- Output: `unknown`\n\n#### setGmailReadStatus\nMark one or more Gmail emails as read or unread.\n- Requires a Google OAuth connection with Gmail modify scope.\n- Accepts one or more message IDs as a comma-separated string or array.\n- Set markAsRead to true to mark as read, false to mark as unread.\n- Input: `{ messageIds: string, markAsRead: boolean, connectionId?: string }`\n- Output: `unknown`\n\n#### setRunTitle\nSet the title of the agent run for the user\'s history\n- Input: `{ title: string }`\n- Output: `unknown`\n\n#### setVariable\nExplicitly set a variable to a given value.\n- Useful for bootstrapping global variables or setting constants.\n- The variable name and value both support variable interpolation.\n- The type field is a UI hint only (controls input widget in the editor).\n- Input: `{ value: string | string[] }`\n- Output: `object`\n\n#### telegramEditMessage\nEdit a previously sent Telegram message. Use with the message ID returned by Send Telegram Message.\n- Only text messages sent by the bot can be edited.\n- The messageId is returned by the Send Telegram Message step.\n- Common pattern: send a "Processing..." message, do work, then edit it with the result.\n- Input: `{ botToken: string, chatId: string, messageId: string, text: string }`\n- Output: `unknown`\n\n#### telegramReplyToMessage\nSend a reply to a specific Telegram message. The reply will be visually threaded in the chat.\n- Use the rawMessage.message_id from the incoming trigger variables to reply to the user\'s message.\n- Especially useful in group chats where replies provide context.\n- Returns the sent message ID, which can be used with Edit Telegram Message.\n- Input: `{ botToken: string, chatId: string, replyToMessageId: string, text: string }`\n- Output: `{ messageId: number }`\n\n#### telegramSendAudio\nSend an audio file to a Telegram chat as music or a voice note via a bot.\n- "audio" mode sends as a standard audio file. "voice" mode sends as a voice message (re-uploads the file for large file support).\n- Input: `{ botToken: string, chatId: string, audioUrl: string, mode: "audio" | "voice", caption?: string }`\n- Output: `unknown`\n\n#### telegramSendFile\nSend a document/file to a Telegram chat via a bot.\n- Input: `{ botToken: string, chatId: string, fileUrl: string, caption?: string }`\n- Output: `unknown`\n\n#### telegramSendImage\nSend an image to a Telegram chat via a bot.\n- Input: `{ botToken: string, chatId: string, imageUrl: string, caption?: string }`\n- Output: `unknown`\n\n#### telegramSendMessage\nSend a text message to a Telegram chat via a bot.\n- Messages are sent using MarkdownV2 formatting. Special characters are auto-escaped.\n- botToken format is "botId:token" \u2014 both parts are required.\n- Returns the sent message ID, which can be used with Edit Telegram Message to update the message later.\n- Input: `{ botToken: string, chatId: string, text: string }`\n- Output: `{ messageId: number }`\n\n#### telegramSendVideo\nSend a video to a Telegram chat via a bot.\n- Input: `{ botToken: string, chatId: string, videoUrl: string, caption?: string }`\n- Output: `unknown`\n\n#### telegramSetTyping\nShow the "typing..." indicator in a Telegram chat via a bot.\n- The typing indicator automatically expires after a few seconds. Use this right before sending a message for a natural feel.\n- Input: `{ botToken: string, chatId: string }`\n- Output: `unknown`\n\n#### textToSpeech\nGenerate an audio file from provided text using a speech model.\n- The text field contains the exact words to be spoken (not instructions).\n- In foreground mode, the audio is displayed to the user. In background mode, the URL is saved to a variable.\n- Input: `{ text: string, intermediateAsset?: boolean, speechModelOverride?: { model: string, config?: object } }`\n- Output: `{ audioUrl: string }`\n\n#### transcribeAudio\nConvert an audio file to text using a transcription model.\n- The prompt field provides optional context to improve transcription accuracy (e.g. language, speaker names, domain).\n- Input: `{ audioUrl: string, prompt: string, transcriptionModelOverride?: { model: string, config?: object } }`\n- Output: `{ text: string }`\n\n#### trimMedia\nTrim an audio or video clip\n- Input: `{ inputUrl: string, start?: number | string, duration?: string | number, intermediateAsset?: boolean }`\n- Output: `{ mediaUrl: string }`\n\n#### updateGmailLabels\nAdd or remove labels on Gmail messages, identified by message IDs or a search query.\n- Requires a Google OAuth connection with Gmail modify scope.\n- Provide either a query (Gmail search syntax) or explicit messageIds to target messages.\n- Label IDs can be label names or Gmail label IDs \u2014 names are resolved automatically.\n- Input: `{ query: string, connectionId?: string, messageIds: string, addLabelIds: string, removeLabelIds: string }`\n- Output: `{ updatedMessageIds: string[] }`\n\n#### uploadDataSourceDocument\nUpload a file into an existing data source from a URL or raw text content.\n- If "file" is a single URL, the file is downloaded from that URL and uploaded.\n- If "file" is any other string, a .txt document is created from that content and uploaded.\n- The block waits (polls) for processing to complete before transitioning, up to 5 minutes.\n- Once processing finishes, vectors are loaded into Milvus so the data source is immediately queryable.\n- Supported file types (when using a URL) are the same as the data source upload UI (PDF, DOCX, TXT, etc.).\n- Input: `{ dataSourceId: string, file: string, fileName: string }`\n- Output: `unknown`\n\n#### upscaleImage\nIncrease the resolution of an image using AI upscaling.\n- Output is re-hosted on the CDN as a PNG.\n- Input: `{ imageUrl: string, targetResolution: "2k" | "4k" | "8k", engine: "standard" | "pro" }`\n- Output: `{ imageUrl: string }`\n\n#### upscaleVideo\nUpscale a video file\n- Input: `{ videoUrl: string, targetResolution: "720p" | "1080p" | "2K" | "4K", engine: "standard" | "pro" | "ultimate" | "flashvsr" | "seedance" | "seedvr2" | "runwayml/upscale-v1", intermediateAsset?: boolean }`\n- Output: `{ videoUrl: string }`\n\n#### videoFaceSwap\nSwap faces in a video file\n- Input: `{ videoUrl: string, faceImageUrl: string, targetIndex: number, engine: string, intermediateAsset?: boolean }`\n- Output: `{ videoUrl: string }`\n\n#### videoRemoveBackground\nRemove or replace background from a video\n- Input: `{ videoUrl: string, newBackground: "transparent" | "image", newBackgroundImageUrl?: string, engine: string, intermediateAsset?: boolean }`\n- Output: `{ videoUrl: string }`\n\n#### videoRemoveWatermark\nRemove a watermark from a video\n- Input: `{ videoUrl: string, engine: string, intermediateAsset?: boolean }`\n- Output: `{ videoUrl: string }`\n\n#### watermarkImage\nOverlay a watermark image onto another image.\n- The watermark is placed at the specified corner with configurable padding and width.\n- Input: `{ imageUrl: string, watermarkImageUrl: string, corner: "top-left" | "top-right" | "bottom-left" | "bottom-right", paddingPx: number, widthPx: number, intermediateAsset?: boolean }`\n- Output: `{ imageUrl: string }`\n\n#### watermarkVideo\nAdd an image watermark to a video\n- Input: `{ videoUrl: string, imageUrl: string, corner: "top-left" | "top-right" | "bottom-left" | "bottom-right", paddingPx: number, widthPx: number, intermediateAsset?: boolean }`\n- Output: `{ videoUrl: string }`\n\n### ActiveCampaign\n\n#### activeCampaignAddNote\nAdd a note to an existing contact in ActiveCampaign.\n- Requires an ActiveCampaign OAuth connection (connectionId).\n- The contact must already exist \u2014 use the contact ID from a previous create or search step.\n- Input: `{ contactId: string, note: string, connectionId?: string }`\n- Output: `unknown`\n\n#### activeCampaignCreateContact\nCreate or sync a contact in ActiveCampaign.\n- Requires an ActiveCampaign OAuth connection (connectionId).\n- If a contact with the email already exists, it may be updated depending on ActiveCampaign settings.\n- Custom fields are passed as a key-value map where keys are field IDs.\n- Input: `{ email: string, firstName: string, lastName: string, phone: string, accountId: string, customFields: object, connectionId?: string }`\n- Output: `{ contactId: string }`\n\n### Airtable\n\n#### airtableCreateUpdateRecord\nCreate a new record or update an existing record in an Airtable table.\n- If recordId is provided, updates that record. Otherwise, creates a new one.\n- When updating with updateMode "onlySpecified", unspecified fields are left as-is. With "all", unspecified fields are cleared.\n- Array fields (e.g. multipleAttachments) accept arrays of values.\n- Input: `{ connectionId?: string, baseId: string, tableId: string, recordId?: string, updateMode?: "onlySpecified" | "all", fields: unknown, recordData: object }`\n- Output: `{ recordId: string }`\n\n#### airtableDeleteRecord\nDelete a record from an Airtable table by its record ID.\n- Requires an active Airtable OAuth connection (connectionId).\n- Silently succeeds if the record does not exist.\n- Input: `{ connectionId?: string, baseId: string, tableId: string, recordId: string }`\n- Output: `{ deleted: boolean }`\n\n#### airtableGetRecord\nFetch a single record from an Airtable table by its record ID.\n- Requires an active Airtable OAuth connection (connectionId).\n- If the record is not found, returns a string message instead of a record object.\n- Input: `{ connectionId?: string, baseId: string, tableId: string, recordId: string }`\n- Output: `{ record: { id: string, createdTime: string, fields: object } | null }`\n\n#### airtableGetTableRecords\nFetch multiple records from an Airtable table with optional pagination.\n- Requires an active Airtable OAuth connection (connectionId).\n- Default limit is 100 records. Maximum is 1000.\n- When outputFormat is \'csv\', the variable receives CSV text. The direct execution output always returns parsed records.\n- Input: `{ connectionId?: string, baseId: string, tableId: string, outputFormat?: "json" | "csv", limit?: number }`\n- Output: `{ records: { id: string, createdTime: string, fields: object }[] }`\n\n### Apollo\n\n#### enrichPerson\nLook up professional information about a person using Apollo.io. Search by ID, name, LinkedIn URL, email, or domain.\n- At least one search parameter must be provided.\n- Returns enriched data from Apollo including contact details, employment info, and social profiles.\n- Input: `{ params: { id: string, name: string, linkedinUrl: string, email: string, domain: string } }`\n- Output: `{ data: unknown }`\n\n#### peopleSearch\nSearch for people matching specific criteria using Apollo.io. Supports natural language queries and advanced filters.\n- Can use a natural language "smartQuery" which is converted to Apollo search parameters by an AI model.\n- Advanced params can override or supplement the smart query results.\n- Optionally enriches returned people and/or their organizations for additional detail.\n- Results are paginated. Use limit and page to control the result window.\n- Input: `{ smartQuery: string, enrichPeople: boolean, enrichOrganizations: boolean, limit: string, page: string, params: { personTitles: string, includeSimilarTitles: string, qKeywords: string, personLocations: string, personSeniorities: string, organizationLocations: string, qOrganizationDomainsList: string, contactEmailStatus: string, organizationNumEmployeesRanges: string, revenueRangeMin: string, revenueRangeMax: string, currentlyUsingAllOfTechnologyUids: string, currentlyUsingAnyOfTechnologyUids: string, currentlyNotUsingAnyOfTechnologyUids: string } }`\n- Output: `{ results: unknown }`\n\n### Coda\n\n#### codaCreateUpdatePage\nCreate a new page or update an existing page in a Coda document.\n- Requires a Coda OAuth connection (connectionId).\n- If pageData.pageId is provided, updates that page. Otherwise, creates a new one.\n- Page content is provided as markdown and converted to Coda\'s canvas format.\n- When updating, insertionMode controls how content is applied (default: \'append\').\n- Input: `{ connectionId?: string, pageData: { docId: string, pageId?: string, name: string, subtitle: string, iconName: string, imageUrl: string, parentPageId?: string, pageContent: string | unknown, contentUpdate?: unknown, insertionMode?: string } }`\n- Output: `{ pageId: string }`\n\n#### codaCreateUpdateRow\nCreate a new row or update an existing row in a Coda table.\n- Requires a Coda OAuth connection (connectionId).\n- If rowId is provided, updates that row. Otherwise, creates a new one.\n- Row data keys are column IDs. Empty values are excluded.\n- Input: `{ connectionId?: string, docId: string, tableId: string, rowId?: string, rowData: object }`\n- Output: `{ rowId: string }`\n\n#### codaFindRow\nSearch for a row in a Coda table by matching column values.\n- Requires a Coda OAuth connection (connectionId).\n- Returns the first row matching all specified column values, or null if no match.\n- Search criteria in rowData are ANDed together.\n- Input: `{ connectionId?: string, docId: string, tableId: string, rowData: object }`\n- Output: `{ row: { id: string, values: object } | null }`\n\n#### codaGetPage\nExport and read the contents of a page from a Coda document.\n- Requires a Coda OAuth connection (connectionId).\n- Page export is asynchronous on Coda\'s side \u2014 there may be a brief delay while it processes.\n- If a page was just created in a prior step, there is an automatic 20-second retry if the first export attempt fails.\n- Input: `{ connectionId?: string, docId: string, pageId: string, outputFormat?: "html" | "markdown" }`\n- Output: `{ content: string }`\n\n#### codaGetTableRows\nFetch rows from a Coda table with optional pagination.\n- Requires a Coda OAuth connection (connectionId).\n- Default limit is 10000 rows. Rows are fetched in pages of 500.\n- When outputFormat is \'csv\', the variable receives CSV text. The direct execution output always returns parsed rows.\n- Input: `{ connectionId?: string, docId: string, tableId: string, limit?: number | string, outputFormat?: "json" | "csv" }`\n- Output: `{ rows: { id: string, values: object }[] }`\n\n### Facebook\n\n#### scrapeFacebookPage\nScrape a Facebook page\n- Input: `{ pageUrl: string }`\n- Output: `{ data: unknown }`\n\n#### scrapeFacebookPosts\nGet all the posts for a Facebook page\n- Input: `{ pageUrl: string }`\n- Output: `{ data: unknown }`\n\n### Gmail\n\n#### deleteGmailEmail\nMove an email to trash in the connected Gmail account (recoverable delete).\n- Requires a Google OAuth connection with Gmail modify scope.\n- Uses trash (recoverable) rather than permanent delete.\n- Input: `{ messageId: string, connectionId?: string }`\n- Output: `unknown`\n\n#### getGmailDraft\nRetrieve a specific draft from Gmail by draft ID.\n- Requires a Google OAuth connection with Gmail readonly scope.\n- Returns the draft content including subject, recipients, sender, and body.\n- Input: `{ draftId: string, connectionId?: string }`\n- Output: `{ draftId: string, messageId: string, subject: string, to: string, from: string, body: string }`\n\n#### getGmailEmail\nRetrieve a specific email from Gmail by message ID.\n- Requires a Google OAuth connection with Gmail readonly scope.\n- Returns the email subject, sender, recipient, date, body (plain text preferred, falls back to HTML), and labels.\n- Input: `{ messageId: string, connectionId?: string }`\n- Output: `{ messageId: string, subject: string, from: string, to: string, date: string, body: string, labels: string }`\n\n#### listGmailDrafts\nList drafts in the connected Gmail account.\n- Requires a Google OAuth connection with Gmail readonly scope.\n- Returns up to 50 drafts (default 10).\n- The variable receives text or JSON depending on exportType.\n- Input: `{ connectionId?: string, limit?: string, exportType: "json" | "text" }`\n- Output: `{ drafts: { draftId: string, messageId: string, subject: string, to: string, snippet: string }[] }`\n\n#### replyToGmailEmail\nReply to an existing email in Gmail. The reply is threaded under the original message.\n- Requires a Google OAuth connection with Gmail compose and readonly scopes.\n- The reply is sent to the original sender and threaded under the original message.\n- messageType controls the body format: "plain", "html", or "markdown".\n- Input: `{ messageId: string, message: string, messageType: "plain" | "html" | "markdown", connectionId?: string }`\n- Output: `{ messageId: string }`\n\n### Google\n\n#### createGoogleDoc\nCreate a new Google Document and optionally populate it with content.\n- textType determines how the text field is interpreted: "plain" for plain text, "html" for HTML markup, "markdown" for Markdown.\n- Input: `{ title: string, text: string, connectionId?: string, textType: "plain" | "html" | "markdown" }`\n- Output: `{ documentUrl: string }`\n\n#### createGoogleSheet\nCreate a new Google Spreadsheet and populate it with CSV data.\n- Input: `{ title: string, text: string, connectionId?: string }`\n- Output: `{ spreadsheetUrl: string }`\n\n#### deleteGoogleSheetRows\nDelete a range of rows from a Google Spreadsheet.\n- Requires a Google OAuth connection with Drive scope.\n- startRow and endRow are 1-based row numbers (inclusive).\n- If sheetName is omitted, operates on the first sheet.\n- Input: `{ documentId: string, sheetName?: string, startRow: string, endRow: string, connectionId?: string }`\n- Output: `unknown`\n\n#### fetchGoogleDoc\nFetch the contents of an existing Google Document.\n- exportType controls the output format: "html" for HTML markup, "markdown" for Markdown, "json" for structured JSON, "plain" for plain text.\n- Input: `{ documentId: string, connectionId?: string, exportType: "html" | "markdown" | "json" | "plain" }`\n- Output: `{ content: string }`\n\n#### fetchGoogleSheet\nFetch contents of a Google Spreadsheet range.\n- range uses A1 notation (e.g. "Sheet1!A1:C10"). Omit to fetch the entire first sheet.\n- exportType controls the output format: "csv" for comma-separated values, "json" for structured JSON.\n- Input: `{ spreadsheetId: string, range: string, connectionId?: string, exportType: "csv" | "json" }`\n- Output: `{ content: string }`\n\n#### getGoogleSheetInfo\nGet metadata about a Google Spreadsheet including sheet names, row counts, and column counts.\n- Requires a Google OAuth connection with Drive scope.\n- Returns the spreadsheet title and a list of all sheets with their dimensions.\n- Input: `{ documentId: string, connectionId?: string }`\n- Output: `{ title: string, sheets: { sheetId: number, title: string, rowCount: number, columnCount: number }[] }`\n\n#### updateGoogleDoc\nUpdate the contents of an existing Google Document.\n- operationType controls how content is applied: "addToTop" prepends, "addToBottom" appends, "overwrite" replaces all content.\n- textType determines how the text field is interpreted: "plain" for plain text, "html" for HTML markup, "markdown" for Markdown.\n- Input: `{ documentId: string, connectionId?: string, text: string, textType: "plain" | "html" | "markdown", operationType: "addToTop" | "addToBottom" | "overwrite" }`\n- Output: `{ documentUrl: string }`\n\n#### updateGoogleSheet\nUpdate a Google Spreadsheet with new data.\n- operationType controls how data is written: "addToBottom" appends rows, "overwrite" replaces all data, "range" writes to a specific cell range.\n- Data should be provided as CSV in the text field.\n- Input: `{ text: string, connectionId?: string, spreadsheetId: string, range: string, operationType: "addToBottom" | "overwrite" | "range" }`\n- Output: `{ spreadsheetUrl: string }`\n\n### Google Calendar\n\n#### createGoogleCalendarEvent\nCreate a new event on a Google Calendar.\n- Requires a Google OAuth connection with Calendar events scope.\n- Date/time values must be ISO 8601 format (e.g. "2025-07-02T10:00:00-07:00").\n- Attendees are specified as one email address per line in a single string.\n- Set addMeetLink to true to automatically attach a Google Meet video call.\n- Input: `{ connectionId?: string, summary: string, description?: string, location?: string, startDateTime: string, endDateTime: string, attendees?: string, addMeetLink?: boolean, calendarId?: string }`\n- Output: `{ eventId: string, htmlLink: string }`\n\n#### deleteGoogleCalendarEvent\nRetrieve a specific event from a Google Calendar by event ID.\n- Requires a Google OAuth connection with Calendar events scope.\n- The variable receives JSON or XML-like text depending on exportType. The direct execution output always returns the structured event.\n- Input: `{ connectionId?: string, eventId: string, calendarId?: string }`\n- Output: `unknown`\n\n#### getGoogleCalendarEvent\nRetrieve a specific event from a Google Calendar by event ID.\n- Requires a Google OAuth connection with Calendar events scope.\n- The variable receives JSON or XML-like text depending on exportType. The direct execution output always returns the structured event.\n- Input: `{ connectionId?: string, eventId: string, exportType: "json" | "text", calendarId?: string }`\n- Output: `{ event: { id?: string | null, status?: string | null, htmlLink?: string | null, created?: string | null, updated?: string | null, summary?: string | null, description?: string | null, location?: string | null, organizer?: { displayName?: string | null, email?: string | null } | null, start?: { dateTime?: string | null, timeZone?: string | null } | null, end?: { dateTime?: string | null, timeZone?: string | null } | null, attendees?: ({ displayName?: string | null, email?: string | null, responseStatus?: string | null })[] | null } }`\n\n#### listGoogleCalendarEvents\nList upcoming events from a Google Calendar, ordered by start time.\n- Requires a Google OAuth connection with Calendar events scope.\n- Only returns future events (timeMin = now).\n- The variable receives JSON or XML-like text depending on exportType. The direct execution output always returns structured events.\n- Input: `{ connectionId?: string, limit: number, exportType: "json" | "text", calendarId?: string }`\n- Output: `{ events: ({ id?: string | null, status?: string | null, htmlLink?: string | null, created?: string | null, updated?: string | null, summary?: string | null, description?: string | null, location?: string | null, organizer?: { displayName?: string | null, email?: string | null } | null, start?: { dateTime?: string | null, timeZone?: string | null } | null, end?: { dateTime?: string | null, timeZone?: string | null } | null, attendees?: ({ displayName?: string | null, email?: string | null, responseStatus?: string | null })[] | null })[] }`\n\n#### searchGoogleCalendarEvents\nSearch for events in a Google Calendar by keyword, date range, or both.\n- Requires a Google OAuth connection with Calendar events scope.\n- Supports keyword search via "query" and date filtering via "timeMin"/"timeMax" (ISO 8601 format).\n- Unlike "List Events" which only shows future events, this allows searching past events too.\n- Input: `{ query?: string, timeMin?: string, timeMax?: string, calendarId?: string, limit?: number, exportType: "json" | "text", connectionId?: string }`\n- Output: `{ events: ({ id?: string | null, status?: string | null, htmlLink?: string | null, created?: string | null, updated?: string | null, summary?: string | null, description?: string | null, location?: string | null, organizer?: { displayName?: string | null, email?: string | null } | null, start?: { dateTime?: string | null, timeZone?: string | null } | null, end?: { dateTime?: string | null, timeZone?: string | null } | null, attendees?: ({ displayName?: string | null, email?: string | null, responseStatus?: string | null })[] | null })[] }`\n\n#### updateGoogleCalendarEvent\nUpdate an existing event on a Google Calendar. Only specified fields are changed.\n- Requires a Google OAuth connection with Calendar events scope.\n- Fetches the existing event first, then applies only the provided updates. Omitted fields are left unchanged.\n- Attendees are specified as one email address per line, and replace the entire attendee list.\n- Input: `{ connectionId?: string, eventId: string, summary?: string, description?: string, location?: string, startDateTime?: string, endDateTime?: string, attendees?: string, calendarId?: string }`\n- Output: `{ eventId: string, htmlLink: string }`\n\n### Google Drive\n\n#### getGoogleDriveFile\nDownload a file from Google Drive and rehost it on the CDN. Returns a public CDN URL.\n- Requires a Google OAuth connection with Drive scope.\n- Google-native files (Docs, Sheets, Slides) cannot be downloaded \u2014 use dedicated steps instead.\n- Maximum file size: 200MB.\n- The file is downloaded and re-uploaded to the CDN; the returned URL is publicly accessible.\n- Input: `{ fileId: string, connectionId?: string }`\n- Output: `{ url: string, name: string, mimeType: string, size: number }`\n\n#### listGoogleDriveFiles\nList files in a Google Drive folder.\n- Requires a Google OAuth connection with Drive scope.\n- If folderId is omitted, lists files in the root folder.\n- Returns file metadata including name, type, size, and links.\n- Input: `{ folderId?: string, limit?: number, connectionId?: string, exportType: "json" | "text" }`\n- Output: `{ files: { id: string, name: string, mimeType: string, size: string, webViewLink: string, createdTime: string, modifiedTime: string }[] }`\n\n#### searchGoogleDrive\nSearch for files in Google Drive by keyword.\n- Requires a Google OAuth connection with Drive scope.\n- Searches file content and names using Google Drive\'s fullText search.\n- Input: `{ query: string, limit?: number, connectionId?: string, exportType: "json" | "text" }`\n- Output: `{ files: { id: string, name: string, mimeType: string, size: string, webViewLink: string, createdTime: string, modifiedTime: string }[] }`\n\n### HubSpot\n\n#### hubspotCreateCompany\nCreate a new company or update an existing one in HubSpot. Matches by domain.\n- Requires a HubSpot OAuth connection (connectionId).\n- If a company with the given domain already exists, it is updated. Otherwise, a new one is created.\n- Property values are type-checked against enabledProperties before being sent to HubSpot.\n- Input: `{ connectionId?: string, company: { domain: string, name: string }, enabledProperties: ({ label: string, value: string, type: "string" | "number" | "bool" })[] }`\n- Output: `{ companyId: string }`\n\n#### hubspotCreateContact\nCreate a new contact or update an existing one in HubSpot. Matches by email address.\n- Requires a HubSpot OAuth connection (connectionId).\n- If a contact with the given email already exists, it is updated. Otherwise, a new one is created.\n- If companyDomain is provided, the contact is associated with that company (creating the company if needed).\n- Property values are type-checked against enabledProperties before being sent to HubSpot.\n- Input: `{ connectionId?: string, contact: { email: string, firstname: string, lastname: string }, enabledProperties: ({ label: string, value: string, type: "string" | "number" | "bool" })[], companyDomain: string }`\n- Output: `{ contactId: string }`\n\n#### hubspotGetCompany\nLook up a HubSpot company by domain name or company ID.\n- Requires a HubSpot OAuth connection (connectionId).\n- Returns null if the company is not found.\n- When searching by domain, performs a search query then fetches the full company record.\n- Use additionalProperties to request specific HubSpot properties beyond the defaults.\n- Input: `{ connectionId?: string, searchBy: "domain" | "id", companyDomain: string, companyId: string, additionalProperties: string[] }`\n- Output: `{ company: { id: string, properties: object, createdAt: string, updatedAt: string, archived: boolean } | null }`\n\n#### hubspotGetContact\nLook up a HubSpot contact by email address or contact ID.\n- Requires a HubSpot OAuth connection (connectionId).\n- Returns null if the contact is not found.\n- Use additionalProperties to request specific HubSpot properties beyond the defaults.\n- Input: `{ connectionId?: string, searchBy: "email" | "id", contactEmail: string, contactId: string, additionalProperties: string[] }`\n- Output: `{ contact: { id: string, properties: object, createdAt: string, updatedAt: string, archived: boolean } | null }`\n\n### Hunter.io\n\n#### hunterApiCompanyEnrichment\nLook up company information by domain using Hunter.io.\n- Returns company name, description, location, industry, size, technologies, and more.\n- If the domain input is a full URL, the hostname is automatically extracted.\n- Returns null if the company is not found.\n- Input: `{ domain: string }`\n- Output: `{ data: { name: string, domain: string, description: string | null, country: string | null, state: string | null, city: string | null, industry: string | null, employees_range: string | null, logo_url: string | null, technologies: string[] } | null }`\n\n#### hunterApiDomainSearch\nSearch for email addresses associated with a domain using Hunter.io.\n- If the domain input is a full URL, the hostname is automatically extracted.\n- Returns a list of email addresses found for the domain along with organization info.\n- Input: `{ domain: string }`\n- Output: `{ data: { domain: string, disposable: boolean, webmail: boolean, accept_all: boolean, pattern: string, organization: string, country: string | null, state: string | null, emails: ({ value: string, type: string, confidence: number, first_name: string | null, last_name: string | null, position: string | null, seniority: string | null, department: string | null, linkedin: string | null, twitter: string | null, phone_number: string | null })[], linked_domains: string[] } }`\n\n#### hunterApiEmailFinder\nFind an email address for a specific person at a domain using Hunter.io.\n- Requires a first name, last name, and domain.\n- If the domain input is a full URL, the hostname is automatically extracted.\n- Returns the most likely email address with a confidence score.\n- Input: `{ domain: string, firstName: string, lastName: string }`\n- Output: `{ data: { first_name: string, last_name: string, email: string, score: number, domain: string, accept_all: boolean, position: string | null, twitter: string | null, linkedin_url: string | null, phone_number: string | null, company: string | null, sources: { domain: string, uri: string, extracted_on: string }[] } }`\n\n#### hunterApiEmailVerification\nVerify whether an email address is valid and deliverable using Hunter.io.\n- Checks email format, MX records, SMTP server, and mailbox deliverability.\n- Returns a status ("valid", "invalid", "accept_all", "webmail", "disposable", "unknown") and a score.\n- Input: `{ email: string }`\n- Output: `{ data: { status: string, result: string, score: number, email: string, regexp: boolean, gibberish: boolean, disposable: boolean, webmail: boolean, mx_records: boolean, smtp_server: boolean, smtp_check: boolean, accept_all: boolean, block: boolean, sources: { domain: string, uri: string, extracted_on: string }[] } }`\n\n#### hunterApiPersonEnrichment\nLook up professional information about a person by their email address using Hunter.io.\n- Returns name, job title, social profiles, and company information.\n- If the person is not found, returns an object with an error message instead of throwing.\n- Input: `{ email: string }`\n- Output: `{ data: { first_name: string, last_name: string, email: string, position: string | null, seniority: string | null, department: string | null, linkedin_url: string | null, twitter: string | null, phone_number: string | null, company: { name: string, domain: string, industry: string | null } | null } | { error: string } }`\n\n### Instagram\n\n#### scrapeInstagramComments\nGet all the comments for an Instagram post\n- Input: `{ postUrl: string, resultsLimit: string }`\n- Output: `{ data: unknown }`\n\n#### scrapeInstagramMentions\nScrape an Instagram profile\'s mentions\n- Input: `{ profileUrl: string, resultsLimit: string }`\n- Output: `{ data: unknown }`\n\n#### scrapeInstagramPosts\nGet all the posts for an Instagram profile\n- Input: `{ profileUrl: string, resultsLimit: string, onlyPostsNewerThan: string }`\n- Output: `{ data: unknown }`\n\n#### scrapeInstagramProfile\nScrape an Instagram profile\n- Input: `{ profileUrl: string }`\n- Output: `{ data: unknown }`\n\n#### scrapeInstagramReels\nGet all the reels for an Instagram profile\n- Input: `{ profileUrl: string, resultsLimit: string }`\n- Output: `{ data: unknown }`\n\n### LinkedIn\n\n#### postToLinkedIn\nCreate a post on LinkedIn from the connected account.\n- Requires a LinkedIn OAuth connection (connectionId).\n- Supports text posts, image posts, video posts, document posts, and article posts.\n- Attach one media type per post: image, video, document, or article.\n- Documents support PDF, PPT, PPTX, DOC, DOCX (max 100MB, 300 pages). Displays as a slideshow carousel.\n- Articles create a link preview with optional custom title, description, and thumbnail.\n- Visibility controls who can see the post.\n- Input: `{ message: string, visibility: "PUBLIC" | "CONNECTIONS", imageUrl?: string, videoUrl?: string, documentUrl?: string, articleUrl?: string, titleText?: string, descriptionText?: string, connectionId?: string }`\n- Output: `unknown`\n\n### Meta Threads\n\n#### scrapeMetaThreadsProfile\nScrape a Meta Threads profile\n- Input: `{ profileUrl: string }`\n- Output: `{ data: unknown }`\n\n### Notion\n\n#### notionCreatePage\nCreate a new page in Notion as a child of an existing page.\n- Requires a Notion OAuth connection (connectionId).\n- Content is provided as markdown and converted to Notion blocks (headings, paragraphs, lists, code, quotes).\n- The page is created as a child of the specified parent page (pageId).\n- Input: `{ pageId: string, content: string, title: string, connectionId?: string }`\n- Output: `{ pageId: string, pageUrl: string }`\n\n#### notionUpdatePage\nUpdate the content of an existing Notion page.\n- Requires a Notion OAuth connection (connectionId).\n- Content is provided as markdown and converted to Notion blocks.\n- "append" mode adds content to the end of the page. "overwrite" mode deletes all existing blocks first.\n- Input: `{ pageId: string, content: string, mode: "append" | "overwrite", connectionId?: string }`\n- Output: `{ pageId: string, pageUrl: string }`\n\n### X\n\n#### postToX\nCreate a post on X (Twitter) from the connected account.\n- Requires an X OAuth connection (connectionId).\n- Maximum 280 characters of text.\n- Optionally attach up to 4 media items (images, GIFs, or videos) via mediaUrls.\n- Media URLs must be publicly accessible. The service fetches and uploads them to X.\n- Supported formats: JPEG, PNG, GIF, WEBP, MP4. Images up to 5MB, videos up to 512MB.\n- Input: `{ text: string, connectionId?: string, mediaUrls?: string[] }`\n- Output: `unknown`\n\n#### searchXPosts\nSearch recent X (Twitter) posts matching a query.\n- Searches only the past 7 days of posts.\n- Query supports X API v2 search operators (up to 512 characters).\nAvailable search operators in query:\n| Operator | Description |\n| -----------------| -------------------------------------------------|\n| from: | Posts from a specific user (e.g., from:elonmusk) |\n| to: | Posts sent to a specific user (e.g., to:NASA) |\n| @ | Mentions a user (e.g., @openai) |\n| # | Hashtag search (e.g., #AI) |\n| is:retweet | Filters retweets |\n| is:reply | Filters replies |\n| has:media | Posts containing media (images, videos, or GIFs) |\n| has:links | Posts containing URLs |\n| lang: | Filters by language (e.g., lang:en) |\n| - | Excludes specific terms (e.g., -spam) |\n| () | Groups terms or operators (e.g., (AI OR ML)) |\n| AND, OR, NOT | Boolean logic for combining or excluding terms |\nConjunction-Required Operators (must be combined with a standalone operator):\n| Operator | Description |\n| ------------ | -----------------------------------------------|\n| has:media | Posts containing media (images, videos, or GIFs) |\n| has:links | Posts containing URLs |\n| is:retweet | Filters retweets |\n| is:reply | Filters replies |\nFor example, has:media alone is invalid, but #AI has:media is valid.\n- Input: `{ query: string, scope: "recent" | "all", options: { startTime?: string, endTime?: string, maxResults?: number } }`\n- Output: `{ posts: { id: string, authorId: string, dateCreated: string, text: string, stats: { retweets: number, replies: number, likes: number } }[] }`\n\n### YouTube\n\n#### fetchYoutubeCaptions\nRetrieve the captions/transcript for a YouTube video.\n- Supports multiple languages via the language parameter.\n- "text" export produces timestamped plain text; "json" export produces structured transcript data.\n- Input: `{ videoUrl: string, exportType: "text" | "json", language: string }`\n- Output: `{ transcripts: { text: string, start: number }[] }`\n\n#### fetchYoutubeChannel\nRetrieve metadata and recent videos for a YouTube channel.\n- Accepts a YouTube channel URL (e.g. https://www.youtube.com/@ChannelName or /channel/ID).\n- Returns channel info and video listings as a JSON object.\n- Input: `{ channelUrl: string }`\n- Output: `object`\n\n#### fetchYoutubeComments\nRetrieve comments for a YouTube video.\n- Paginates through comments (up to 5 pages).\n- "text" export produces markdown-formatted text; "json" export produces structured comment data.\n- Input: `{ videoUrl: string, exportType: "text" | "json", limitPages: string }`\n- Output: `{ comments: { id: string, link: string, publishedDate: string, text: string, likes: number, replies: number, author: string, authorLink: string, authorImg: string }[] }`\n\n#### fetchYoutubeVideo\nRetrieve metadata for a YouTube video (title, description, stats, channel info).\n- Returns video metadata, channel info, and engagement stats.\n- Video format data is excluded from the response.\n- Input: `{ videoUrl: string }`\n- Output: `object`\n\n#### searchYoutube\nSearch for YouTube videos by keyword.\n- Supports pagination (up to 5 pages) and country/language filters.\n- Use the filter/filterType fields for YouTube search parameter (sp) filters.\n- Input: `{ query: string, limitPages: string, filter: string, filterType: string, countryCode?: string, languageCode?: string }`\n- Output: `{ results: object }`\n\n#### searchYoutubeTrends\nRetrieve trending videos on YouTube by category and region.\n- Categories: "now" (trending now), "music", "gaming", "films".\n- Supports country and language filtering.\n- Input: `{ bp: "now" | "music" | "gaming" | "films", hl: string, gl: string }`\n- Output: `object`\n\n### Helpers\n\n#### `listModels()`\nList all available AI models across all categories.\n\nOutput:\n```typescript\n{\n models: {\n id: string;\n name: string; // Display name\n type: "llm_chat" | "image_generation" | "video_generation" | "video_analysis" | "text_to_speech" | "vision" | "transcription";\n maxTemperature: number;\n maxResponseSize: number;\n inputs: object[]; // Accepted input types\n }[]\n}\n```\n\n#### `listModelsByType(modelType)`\nList AI models filtered by type.\n- `modelType`: `"llm_chat"` | `"image_generation"` | `"video_generation"` | `"video_analysis"` | `"text_to_speech"` | `"vision"` | `"transcription"`\n- Output: same as `listModels()`\n\n#### `listModelsSummary()`\nList all available AI models (summary). Returns only id, name, type, and tags. Suitable for display or consumption inside a model context window.\n\nOutput:\n```typescript\n{\n models: {\n id: string;\n name: string;\n type: "llm_chat" | "image_generation" | "video_generation" | "video_analysis" | "text_to_speech" | "vision" | "transcription";\n tags: string; // Comma-separated tags\n }[]\n}\n```\n\n#### `listModelsSummaryByType(modelType)`\nList AI models (summary) filtered by type.\n- `modelType`: `"llm_chat"` | `"image_generation"` | `"video_generation"` | `"video_analysis"` | `"text_to_speech"` | `"vision"` | `"transcription"`\n- Output: same as `listModelsSummary()`\n\n#### `listConnectors()`\nList available OAuth connector services (Slack, Google, HubSpot, etc.) and their actions. These are third-party integrations \u2014 for most tasks, use actions directly instead.\n\nOutput:\n```typescript\n{\n services: {\n id: string;\n name: string;\n icon: string;\n actions: { id: string; name: string }[];\n }[]\n}\n```\n\n#### `getConnector(serviceId)`\nGet details for a single OAuth connector service by ID.\n\nOutput:\n```typescript\n{\n service: {\n id: string;\n name: string;\n icon: string;\n actions: { id: string; name: string }[];\n }\n}\n```\n\n#### `getConnectorAction(serviceId, actionId)`\nGet the full configuration for an OAuth connector action, including all input fields needed to call it via `runFromConnectorRegistry`. OAuth connectors are sourced from the open-source MindStudio Connector Registry (MSCR) with 850+ actions across third-party services.\n\nOutput:\n```typescript\n{\n action: {\n id: string;\n name: string;\n description: string;\n quickHelp: string;\n configuration: { title: string; items: { label: string; helpText: string; variable: string; type: string; defaultValue: string; placeholder: string; selectOptions?: object }[] }[];\n }\n}\n```\n\n#### `listConnections()`\nList OAuth connections for the organization (authenticated third-party service links). Use the returned connection IDs when calling OAuth connector actions. Connectors require the user to connect to the third-party service in MindStudio before they can be used.\n\nOutput:\n```typescript\n{\n connections: {\n id: string; // Connection ID to pass to connector actions\n provider: string; // Integration provider (e.g. slack, google)\n name: string; // Display name or account identifier\n }[]\n}\n```\n\n#### `estimateStepCost(stepType, step?, options?)`\nEstimate the cost of executing a step before running it. Pass the same step config you would use for execution.\n\n```typescript\nconst estimate = await mindstudio.estimateStepCost(\'generateText\', { message: \'Hello\' });\n```\n\n- `stepType`: string \u2014 The action name (e.g. `"generateText"`).\n- `step`: object \u2014 Optional action input parameters for more accurate estimates.\n- `options`: `{ appId?: string, workflowId?: string }` \u2014 Optional context for pricing.\n\nOutput:\n```typescript\n{\n costType?: string; // "free" when the step has no cost\n estimates?: {\n eventType: string; // Billing event type\n label: string; // Human-readable cost label\n unitPrice: number; // Price per unit in billing units\n unitType: string; // What constitutes a unit (e.g. "token", "request")\n estimatedCost?: number; // Estimated total cost, or null if not estimable\n quantity: number; // Number of billable units\n }[]\n}\n```\n\n#### `changeName(displayName)`\nUpdate the display name of the authenticated agent. Useful for agents to set their own name after connecting.\n\n```typescript\nawait mindstudio.changeName(\'My Agent\');\n```\n\n#### `changeProfilePicture(profilePictureUrl)`\nUpdate the profile picture of the authenticated agent. Useful for agents to set their own avatar after connecting.\n\n```typescript\nawait mindstudio.changeProfilePicture(\'https://example.com/avatar.png\');\n```\n\n#### `uploadFile(content, options)`\nUpload a file to the MindStudio CDN. Gets a signed upload URL, PUTs the file content, and returns the permanent public URL.\n\n```typescript\nimport { readFileSync } from \'fs\';\nconst { url } = await mindstudio.uploadFile(readFileSync(\'photo.png\'), { extension: \'png\', type: \'image/png\' });\n```\n\n- `content`: `Buffer | Uint8Array` \u2014 The file content.\n- `options.extension`: string \u2014 File extension without the dot (e.g. `"png"`, `"jpg"`, `"mp4"`).\n- `options.type`: string (optional) \u2014 MIME type (e.g. `"image/png"`). Determines which CDN subdomain is used.\n\nOutput: `{ url: string }` \u2014 The permanent public CDN URL.\n';
5497
5626
  }
5498
5627
  });
5499
5628
 
@@ -5750,7 +5879,7 @@ async function startMcpServer(options) {
5750
5879
  capabilities: { tools: {} },
5751
5880
  serverInfo: {
5752
5881
  name: "mindstudio-agent",
5753
- version: "0.1.58"
5882
+ version: "0.1.60"
5754
5883
  },
5755
5884
  instructions: 'Welcome to MindStudio \u2014 a platform with 200+ AI models, 850+ third-party integrations, and pre-built agents.\n\nGetting started:\n1. Call `ask` with any question about the SDK \u2014 it knows every action, model, and connector and returns working code with real model IDs and config options. Examples: ask("generate an image with FLUX"), ask("what models support vision?"), ask("how do I send a Slack message?").\n2. Call `changeName` to set your display name \u2014 use your name or whatever your user calls you. This is how you\'ll appear in MindStudio request logs.\n3. If you have a profile picture or icon, call `uploadFile` to upload it, then `changeProfilePicture` with the returned URL.\n4. For manual browsing, call `listActions` to discover all available actions.\n\nThen use the tools to generate text, images, video, audio, search the web, work with data sources, run agents, and more.\n\nImportant:\n- AI-powered actions (text generation, image generation, video, audio, etc.) cost money. Before running these, call `estimateActionCost` and confirm with the user before proceeding \u2014 unless they\'ve explicitly told you to go ahead.\n- Not all agents from `listAgents` are configured for API use. Do not try to run an agent just because it appears in the list \u2014 it will likely fail. Only run agents the user specifically asks you to run.'
5756
5885
  });
@@ -5808,6 +5937,8 @@ async function startMcpServer(options) {
5808
5937
  );
5809
5938
  } else if (toolName === "listConnections") {
5810
5939
  result = await getAgent().listConnections();
5940
+ } else if (toolName === "listPackagedWorkflows") {
5941
+ result = await getAgent().listPackagedWorkflows();
5811
5942
  } else if (toolName === "estimateActionCost") {
5812
5943
  const meta = await getMetadata();
5813
5944
  const rawType = args.stepType;
@@ -5944,6 +6075,7 @@ var init_mcp = __esm({
5944
6075
  getConnector: "Get details for an OAuth connector service.",
5945
6076
  getConnectorAction: "Get full configuration for an OAuth connector action.",
5946
6077
  listConnections: "List OAuth connections for the organization (authenticated third-party service links).",
6078
+ listPackagedWorkflows: "List packaged workflows available to the organization.",
5947
6079
  estimateStepCost: "Estimate the cost of executing an action before running it.",
5948
6080
  changeName: "Update the display name of the authenticated agent.",
5949
6081
  changeProfilePicture: "Update the profile picture of the authenticated agent.",
@@ -6049,6 +6181,11 @@ var init_mcp = __esm({
6049
6181
  description: "List OAuth connections for the organization (authenticated third-party service links). Use the returned connection IDs when calling OAuth connector actions.",
6050
6182
  inputSchema: { type: "object", properties: {} }
6051
6183
  },
6184
+ {
6185
+ name: "listPackagedWorkflows",
6186
+ description: "List packaged workflows available to the organization, including their app/workflow IDs and input/output signatures.",
6187
+ inputSchema: { type: "object", properties: {} }
6188
+ },
6052
6189
  {
6053
6190
  name: "estimateActionCost",
6054
6191
  description: "Estimate the cost of executing an action before running it. Pass the same config you would use for execution.",
@@ -6205,6 +6342,7 @@ Discover:
6205
6342
  list-actions [--json] [--summary] List all available actions
6206
6343
  info <action> Show action details and parameters
6207
6344
  list-models [--type <t>] [--summary] List available AI models
6345
+ list-packaged-workflows List packaged workflows
6208
6346
 
6209
6347
  Batch:
6210
6348
  batch [json] Execute multiple actions in parallel
@@ -6559,6 +6697,11 @@ async function cmdListConnections(options) {
6559
6697
  const result = await agent.listConnections();
6560
6698
  jsonOut(result);
6561
6699
  }
6700
+ async function cmdListPackagedWorkflows(options) {
6701
+ const agent = await createAgent(options);
6702
+ const result = await agent.listPackagedWorkflows();
6703
+ jsonOut(result);
6704
+ }
6562
6705
  async function cmdEstimateStepCost(method, input, options) {
6563
6706
  const agent = await createAgent(options);
6564
6707
  const result = await agent.estimateStepCost(method, input);
@@ -6707,7 +6850,7 @@ function isNewerVersion(current, latest) {
6707
6850
  return false;
6708
6851
  }
6709
6852
  async function checkForUpdate() {
6710
- const currentVersion = "0.1.58";
6853
+ const currentVersion = "0.1.60";
6711
6854
  if (!currentVersion) return null;
6712
6855
  try {
6713
6856
  const { loadConfig: loadConfig2, saveConfig: saveConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
@@ -6736,7 +6879,7 @@ async function checkForUpdate() {
6736
6879
  }
6737
6880
  }
6738
6881
  function printUpdateNotice(latestVersion) {
6739
- const currentVersion = "0.1.58";
6882
+ const currentVersion = "0.1.60";
6740
6883
  process.stderr.write(
6741
6884
  `
6742
6885
  ${ansi2.cyanBright("Update available")} ${ansi2.gray(currentVersion + " \u2192")} ${ansi2.cyanBold(latestVersion)}
@@ -6749,7 +6892,7 @@ function isStandaloneBinary() {
6749
6892
  return !argv1.includes("node_modules");
6750
6893
  }
6751
6894
  async function cmdUpdate() {
6752
- const currentVersion = "0.1.58";
6895
+ const currentVersion = "0.1.60";
6753
6896
  process.stderr.write(
6754
6897
  ` ${ansi2.gray("Current version:")} ${currentVersion}
6755
6898
  `
@@ -6884,7 +7027,7 @@ async function cmdLogin(options) {
6884
7027
  process.stderr.write("\n");
6885
7028
  printLogo();
6886
7029
  process.stderr.write("\n");
6887
- const ver = "0.1.58";
7030
+ const ver = "0.1.60";
6888
7031
  process.stderr.write(
6889
7032
  ` ${ansi2.bold("MindStudio Agent")} ${ver ? " " + ansi2.gray("v" + ver) : ""}
6890
7033
  `
@@ -7211,7 +7354,7 @@ async function main() {
7211
7354
  try {
7212
7355
  if (command === "version" || command === "-v") {
7213
7356
  process.stdout.write(
7214
- "0.1.58\n"
7357
+ "0.1.60\n"
7215
7358
  );
7216
7359
  return;
7217
7360
  }
@@ -7472,6 +7615,13 @@ async function main() {
7472
7615
  });
7473
7616
  return;
7474
7617
  }
7618
+ if (command === "list-packaged-workflows") {
7619
+ await cmdListPackagedWorkflows({
7620
+ apiKey: values["api-key"],
7621
+ baseUrl: values["base-url"]
7622
+ });
7623
+ return;
7624
+ }
7475
7625
  if (command === "estimate-cost") {
7476
7626
  const stepMethod = positionals[1];
7477
7627
  if (!stepMethod)