@mindstudio-ai/agent 0.1.103 → 0.1.105

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.
@@ -792,13 +792,6 @@ var init_metadata = __esm({
792
792
  inputSchema: { "type": "object", "properties": { "webhookUrl": { "type": "string", "description": "Zapier webhook URL to send data to" }, "input": { "type": "object", "properties": {}, "required": [], "description": "Key-value pairs to send as the JSON POST body" } }, "required": ["webhookUrl", "input"] },
793
793
  outputSchema: { "type": "object", "properties": { "data": { "description": "Parsed webhook response from Zapier (JSON object, array, or string)" } }, "required": ["data"] }
794
794
  },
795
- "queryAppDatabase": {
796
- stepType: "queryAppDatabase",
797
- description: "Execute a SQL query against the app managed database.",
798
- usageNotes: '- 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\n and passed as safe parameterized values (preventing SQL injection).\n Example: 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\n into the query string. Use this when another step generates full or partial SQL, e.g.\n a bulk INSERT with a precomputed VALUES list. The user is responsible for sanitization\n when parameterize is false.',
799
- inputSchema: { "type": "object", "properties": { "databaseId": { "type": "string", "description": "Name or ID of the app data database to query" }, "sql": { "type": "string", "description": "SQL query to execute. Use {{variables}} directly in the SQL \u2014 they are handled according to the `parameterize` setting.\n\nWhen parameterize is true (default): {{variables}} are extracted from the SQL, replaced with ? placeholders, resolved via the full MindStudio handlebars pipeline, and passed as safe parameterized values to SQLite. This prevents SQL injection. Example: INSERT INTO contacts (name, email) VALUES ({{name}}, {{email}})\n\nWhen parameterize is false: The entire SQL string is resolved via compileString (standard handlebars interpolation) and executed as-is. Use this for dynamic/generated SQL where another step builds the query. The user is responsible for safety. Example: {{generatedInsertQuery}}\n\nAsk the user for the database schema if they have not already provided it." }, "parameterize": { "type": "boolean", "description": "Whether to treat {{variables}} as parameterized query values (default: true).\n\n- true: {{vars}} are extracted, replaced with ?, and passed as bind params. Safe from SQL injection. Use for standard CRUD operations.\n- false: {{vars}} are interpolated directly into the SQL string via handlebars. Use when another step generates full or partial SQL (e.g. bulk inserts with precomputed VALUES). The user is responsible for sanitization." } }, "required": ["databaseId", "sql"] },
800
- outputSchema: { "type": "object", "properties": { "rows": { "type": "array", "items": {}, "description": "Result rows for SELECT queries (empty array for write queries)" }, "changes": { "type": "number", "description": "Number of rows affected by INSERT, UPDATE, or DELETE queries (0 for SELECT)" } }, "required": ["rows", "changes"] }
801
- },
802
795
  "queryDataSource": {
803
796
  stepType: "queryDataSource",
804
797
  description: "Legacy v1 data sources. v2 apps should use `dataSources.defineDataSource()` from @mindstudio-ai/agent instead, which owns parsing, chunking, embedding and citations. Search a vector data source (RAG) and return relevant document chunks.",
@@ -1002,9 +995,11 @@ var init_metadata = __esm({
1002
995
  - Set replyTo to control the Reply-To address for replies.
1003
996
  - For threaded replies in a shared inbox, set inReplyTo (the Message-ID being replied to) and references (prior Message-IDs in the thread).
1004
997
  - connectionId can be a comma-separated list to send to multiple recipients.
1005
- - The special connectionId "trigger_email" uses the email address that triggered the workflow.`,
1006
- inputSchema: { "type": "object", "properties": { "subject": { "type": "string", "description": "Email subject line" }, "body": { "type": "string", "description": "Email body content (plain text, markdown, HTML, or a CDN URL to an HTML file)" }, "to": { "anyOf": [{ "type": "string" }, { "type": "array", "items": { "type": "string" } }] }, "cc": { "anyOf": [{ "type": "string" }, { "type": "array", "items": { "type": "string" } }] }, "bcc": { "anyOf": [{ "type": "string" }, { "type": "array", "items": { "type": "string" } }] }, "connectionId": { "type": "string", "description": "OAuth connection ID(s) for the recipient(s), comma-separated for multiple" }, "generateHtml": { "type": "boolean", "description": "When true, auto-convert the body text into a styled HTML email using AI" }, "generateHtmlInstructions": { "type": "string", "description": "Natural language instructions for the HTML generation style" }, "generateHtmlModelOverride": { "type": "object", "properties": { "model": { "type": "string", "description": 'Model identifier (e.g. "gpt-4", "claude-3-opus")' }, "temperature": { "type": "number", "description": "Sampling temperature for the model (0-2)" }, "maxResponseTokens": { "type": "number", "description": "Maximum number of tokens in the model's response" }, "ignorePreamble": { "type": "boolean", "description": "Whether to skip the system preamble/instructions" }, "userMessagePreprocessor": { "type": "object", "properties": { "dataSource": { "type": "string", "description": "Data source identifier for the preprocessor" }, "messageTemplate": { "type": "string", "description": "Template string applied to user messages before sending to the model" }, "maxResults": { "type": "number", "description": "Maximum number of results to include from the data source" }, "enabled": { "type": "boolean", "description": "Whether the preprocessor is active" }, "shouldInherit": { "type": "boolean", "description": "Whether child steps should inherit this preprocessor configuration" } }, "description": "Preprocessor applied to user messages before sending to the model" }, "preamble": { "type": "string", "description": "System preamble/instructions for the model" }, "multiModelEnabled": { "type": "boolean", "description": "Whether multi-model candidate generation is enabled" }, "editResponseEnabled": { "type": "boolean", "description": "Whether the user can edit the model's response" }, "config": { "type": "object", "properties": {}, "required": [], "description": "Additional model-specific configuration" } }, "required": ["model", "temperature", "maxResponseTokens"], "description": "Model settings override for HTML generation" }, "attachments": { "type": "array", "items": { "anyOf": [{ "type": "string" }, { "type": "object", "properties": { "url": { "type": "string" }, "filename": { "type": "string" }, "contentType": { "type": "string" } }, "required": ["url"] }] }, "description": "Files to attach: each entry is a URL string, or an object `{ url, filename?, contentType? }` to control the displayed filename and MIME type." }, "from": { "type": "string", "description": "Custom sender handle \u2014 only for apps with a custom domain or subdomain. Bare handle (`support`), full `support@your-domain.com`, or `Name <support@your-domain.com>`. Must resolve to a domain the app owns." }, "bodyType": { "enum": ["auto", "html", "markdown", "text"], "type": "string", "description": "How to interpret `body`: `auto` (default \u2014 detect HTML, else render markdown), `html` (send as-is), `markdown` (render to HTML), or `text` (send as plain text only, no HTML part). Every send includes a text/plain alternative." }, "text": { "type": "string", "description": "Explicit text/plain alternative body. Auto-derived from `body` if omitted." }, "replyTo": { "type": "string", "description": "Reply-To address for the email." }, "inReplyTo": { "type": "string", "description": "Message-ID this email replies to, for inbox threading (In-Reply-To header)." }, "references": { "type": "array", "items": { "type": "string" }, "description": "Prior Message-IDs in the thread, for inbox threading (References header)." } }, "required": ["subject", "body"] },
1007
- outputSchema: { "type": "object", "properties": { "recipients": { "type": "array", "items": { "type": "string" }, "description": "To addresses the message was sent to." }, "cc": { "type": "array", "items": { "type": "string" }, "description": "Cc addresses on the message (empty if none)." }, "bcc": { "type": "array", "items": { "type": "string" }, "description": "Bcc addresses on the message (empty if none)." }, "from": { "type": "string", "description": "The resolved sender address the message went out as (auto-selected when no `from` is given)." } }, "required": ["recipients", "cc", "bcc", "from"] }
998
+ - The special connectionId "trigger_email" uses the email address that triggered the workflow.
999
+ - Sends default to category "marketing": every message carries a one-click unsubscribe header pointing at the app's own domain, and recipients who previously unsubscribed from this app are skipped and returned in "suppressed". Because the unsubscribe link is per-recipient, a marketing send delivers one separate message per recipient and cc/bcc are folded into that list.
1000
+ - Set category to "transactional" for receipts, alerts, password resets and anything a recipient must not be able to opt out of. Transactional sends carry no unsubscribe header, ignore the unsubscribe list, and keep cc/bcc on a single message \u2014 use it whenever cc/bcc semantics matter.`,
1001
+ inputSchema: { "type": "object", "properties": { "subject": { "type": "string", "description": "Email subject line" }, "body": { "type": "string", "description": "Email body content (plain text, markdown, HTML, or a CDN URL to an HTML file)" }, "to": { "anyOf": [{ "type": "string" }, { "type": "array", "items": { "type": "string" } }] }, "cc": { "anyOf": [{ "type": "string" }, { "type": "array", "items": { "type": "string" } }] }, "bcc": { "anyOf": [{ "type": "string" }, { "type": "array", "items": { "type": "string" } }] }, "connectionId": { "type": "string", "description": "OAuth connection ID(s) for the recipient(s), comma-separated for multiple" }, "generateHtml": { "type": "boolean", "description": "When true, auto-convert the body text into a styled HTML email using AI" }, "generateHtmlInstructions": { "type": "string", "description": "Natural language instructions for the HTML generation style" }, "generateHtmlModelOverride": { "type": "object", "properties": { "model": { "type": "string", "description": 'Model identifier (e.g. "gpt-4", "claude-3-opus")' }, "temperature": { "type": "number", "description": "Sampling temperature for the model (0-2)" }, "maxResponseTokens": { "type": "number", "description": "Maximum number of tokens in the model's response" }, "ignorePreamble": { "type": "boolean", "description": "Whether to skip the system preamble/instructions" }, "userMessagePreprocessor": { "type": "object", "properties": { "dataSource": { "type": "string", "description": "Data source identifier for the preprocessor" }, "messageTemplate": { "type": "string", "description": "Template string applied to user messages before sending to the model" }, "maxResults": { "type": "number", "description": "Maximum number of results to include from the data source" }, "enabled": { "type": "boolean", "description": "Whether the preprocessor is active" }, "shouldInherit": { "type": "boolean", "description": "Whether child steps should inherit this preprocessor configuration" } }, "description": "Preprocessor applied to user messages before sending to the model" }, "preamble": { "type": "string", "description": "System preamble/instructions for the model" }, "multiModelEnabled": { "type": "boolean", "description": "Whether multi-model candidate generation is enabled" }, "editResponseEnabled": { "type": "boolean", "description": "Whether the user can edit the model's response" }, "config": { "type": "object", "properties": {}, "required": [], "description": "Additional model-specific configuration" } }, "required": ["model", "temperature", "maxResponseTokens"], "description": "Model settings override for HTML generation" }, "attachments": { "type": "array", "items": { "anyOf": [{ "type": "string" }, { "type": "object", "properties": { "url": { "type": "string" }, "filename": { "type": "string" }, "contentType": { "type": "string" } }, "required": ["url"] }] }, "description": "Files to attach: each entry is a URL string, or an object `{ url, filename?, contentType? }` to control the displayed filename and MIME type." }, "from": { "type": "string", "description": "Custom sender handle \u2014 only for apps with a custom domain or subdomain. Bare handle (`support`), full `support@your-domain.com`, or `Name <support@your-domain.com>`. Must resolve to a domain the app owns." }, "bodyType": { "enum": ["auto", "html", "markdown", "text"], "type": "string", "description": "How to interpret `body`: `auto` (default \u2014 detect HTML, else render markdown), `html` (send as-is), `markdown` (render to HTML), or `text` (send as plain text only, no HTML part). Every send includes a text/plain alternative." }, "text": { "type": "string", "description": "Explicit text/plain alternative body. Auto-derived from `body` if omitted." }, "replyTo": { "type": "string", "description": "Reply-To address for the email." }, "inReplyTo": { "type": "string", "description": "Message-ID this email replies to, for inbox threading (In-Reply-To header)." }, "references": { "type": "array", "items": { "type": "string" }, "description": "Prior Message-IDs in the thread, for inbox threading (References header)." }, "category": { "enum": ["transactional", "marketing"], "type": "string", "description": "`marketing` (default) adds one-click unsubscribe headers and skips recipients who unsubscribed from this app. Use `transactional` for receipts, alerts and password resets: no unsubscribe header, never suppressed, and cc/bcc preserved on a single message." } }, "required": ["subject", "body"] },
1002
+ outputSchema: { "type": "object", "properties": { "recipients": { "type": "array", "items": { "type": "string" }, "description": "To addresses the message was sent to." }, "cc": { "type": "array", "items": { "type": "string" }, "description": "Cc addresses on the message (empty if none)." }, "bcc": { "type": "array", "items": { "type": "string" }, "description": "Bcc addresses on the message (empty if none)." }, "from": { "type": "string", "description": "The resolved sender address the message went out as (auto-selected when no `from` is given)." }, "suppressed": { "type": "array", "items": { "type": "string" }, "description": "Recipients skipped because they unsubscribed from this app. Always empty for a `transactional` send." } }, "required": ["recipients", "cc", "bcc", "from", "suppressed"] }
1008
1003
  },
1009
1004
  "sendGmailDraft": {
1010
1005
  stepType: "sendGmailDraft",
@@ -1795,6 +1790,58 @@ function buildSelect(table, options = {}) {
1795
1790
  if (options.offset != null) sql += ` OFFSET ${options.offset}`;
1796
1791
  return { sql, params: params.length > 0 ? params : void 0 };
1797
1792
  }
1793
+ function assertSqlIdentifier(name, context) {
1794
+ if (!SQL_IDENTIFIER_RE.test(name)) {
1795
+ throw new MindStudioError(
1796
+ `Invalid ${context}: "${name}" \u2014 must be a plain identifier (letters, digits, _ and $, not starting with a digit).`,
1797
+ "invalid_identifier",
1798
+ 400
1799
+ );
1800
+ }
1801
+ }
1802
+ function aggregateExpr(fn, column) {
1803
+ switch (fn) {
1804
+ case "count":
1805
+ return "COUNT(*)";
1806
+ case "sum":
1807
+ return `TOTAL(${column})`;
1808
+ case "avg":
1809
+ return `AVG(${column})`;
1810
+ case "min":
1811
+ return `MIN(${column})`;
1812
+ case "max":
1813
+ return `MAX(${column})`;
1814
+ case "countDistinct":
1815
+ return `COUNT(DISTINCT ${column})`;
1816
+ }
1817
+ }
1818
+ function buildScalarAggregate(table, fn, column, where, whereParams) {
1819
+ if (column != null) assertSqlIdentifier(column, "aggregate column");
1820
+ let sql = `SELECT ${aggregateExpr(fn, column ?? void 0)} AS value FROM ${table}`;
1821
+ if (where) sql += ` WHERE ${where}`;
1822
+ return { sql, params: whereParams?.length ? whereParams : void 0 };
1823
+ }
1824
+ function buildGroupedAggregate(table, options) {
1825
+ for (const col of options.by) assertSqlIdentifier(col, "group-by column");
1826
+ for (const term of options.select) {
1827
+ assertSqlIdentifier(term.alias, "aggregate alias");
1828
+ if (term.column != null) assertSqlIdentifier(term.column, "aggregate column");
1829
+ }
1830
+ if (options.orderBy != null) assertSqlIdentifier(options.orderBy, "aggregate order key");
1831
+ const selectList = [
1832
+ ...options.by,
1833
+ ...options.select.map((t) => `${aggregateExpr(t.fn, t.column)} AS ${t.alias}`)
1834
+ ].join(", ");
1835
+ let sql = `SELECT ${selectList} FROM ${table}`;
1836
+ if (options.where) sql += ` WHERE ${options.where}`;
1837
+ if (options.by.length > 0) sql += ` GROUP BY ${options.by.join(", ")}`;
1838
+ if (options.orderBy) sql += ` ORDER BY ${options.orderBy}${options.desc ? " DESC" : " ASC"}`;
1839
+ if (options.limit != null) sql += ` LIMIT ${options.limit}`;
1840
+ return {
1841
+ sql,
1842
+ params: options.whereParams?.length ? options.whereParams : void 0
1843
+ };
1844
+ }
1798
1845
  function buildExists(table, where, whereParams, negate) {
1799
1846
  const inner = where ? `SELECT 1 FROM ${table} WHERE ${where}` : `SELECT 1 FROM ${table}`;
1800
1847
  const fn = negate ? "NOT EXISTS" : "EXISTS";
@@ -1850,11 +1897,13 @@ function stripSystemColumns(data) {
1850
1897
  }
1851
1898
  return result;
1852
1899
  }
1853
- var USER_PREFIX, SYSTEM_COLUMNS;
1900
+ var USER_PREFIX, SQL_IDENTIFIER_RE, SYSTEM_COLUMNS;
1854
1901
  var init_sql = __esm({
1855
1902
  "src/db/sql.ts"() {
1856
1903
  "use strict";
1904
+ init_errors();
1857
1905
  USER_PREFIX = "@@user@@";
1906
+ SQL_IDENTIFIER_RE = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
1858
1907
  SYSTEM_COLUMNS = /* @__PURE__ */ new Set([
1859
1908
  "id",
1860
1909
  "created_at",
@@ -2466,6 +2515,195 @@ var init_predicate = __esm({
2466
2515
  }
2467
2516
  });
2468
2517
 
2518
+ // src/db/aggregate.ts
2519
+ function normalizeAggregateSpec(spec) {
2520
+ const by = [...spec.by ?? []];
2521
+ for (const col of by) {
2522
+ if (typeof col !== "string" || !IDENTIFIER_RE.test(col)) {
2523
+ throw new MindStudioError(
2524
+ `aggregate(): invalid group-by column "${col}" \u2014 column names are plain identifiers.`,
2525
+ "invalid_column",
2526
+ 400
2527
+ );
2528
+ }
2529
+ }
2530
+ const aliases = Object.keys(spec.select ?? {});
2531
+ if (aliases.length === 0) {
2532
+ throw new MindStudioError(
2533
+ "aggregate(): select must contain at least one aggregate, e.g. { n: { count: true } }.",
2534
+ "invalid_aggregate_spec",
2535
+ 400
2536
+ );
2537
+ }
2538
+ const select = aliases.map((alias) => {
2539
+ if (!IDENTIFIER_RE.test(alias)) {
2540
+ throw new MindStudioError(
2541
+ `aggregate(): invalid alias "${alias}" \u2014 result property names are plain identifiers.`,
2542
+ "invalid_aggregate_alias",
2543
+ 400
2544
+ );
2545
+ }
2546
+ if (by.includes(alias)) {
2547
+ throw new MindStudioError(
2548
+ `aggregate(): alias "${alias}" collides with a group-by column of the same name.`,
2549
+ "invalid_aggregate_alias",
2550
+ 400
2551
+ );
2552
+ }
2553
+ const term = spec.select[alias];
2554
+ const fns = AGGREGATE_FNS.filter((fn2) => term && fn2 in term);
2555
+ if (fns.length !== 1) {
2556
+ throw new MindStudioError(
2557
+ `aggregate(): term "${alias}" must have exactly one aggregate key (count, sum, avg, min, max, countDistinct).`,
2558
+ "invalid_aggregate_spec",
2559
+ 400
2560
+ );
2561
+ }
2562
+ const fn = fns[0];
2563
+ if (fn === "count") {
2564
+ if (term[fn] !== true) {
2565
+ throw new MindStudioError(
2566
+ `aggregate(): term "${alias}" \u2014 count takes \`true\`, e.g. { count: true }.`,
2567
+ "invalid_aggregate_spec",
2568
+ 400
2569
+ );
2570
+ }
2571
+ return { alias, fn };
2572
+ }
2573
+ const column = term[fn];
2574
+ if (typeof column !== "string" || !IDENTIFIER_RE.test(column)) {
2575
+ throw new MindStudioError(
2576
+ `aggregate(): term "${alias}" \u2014 invalid column "${String(column)}".`,
2577
+ "invalid_column",
2578
+ 400
2579
+ );
2580
+ }
2581
+ return { alias, fn, column };
2582
+ });
2583
+ if (spec.orderBy != null) {
2584
+ if (!aliases.includes(spec.orderBy) && !by.includes(spec.orderBy)) {
2585
+ throw new MindStudioError(
2586
+ `aggregate(): orderBy "${spec.orderBy}" is neither a select alias nor a group-by column.`,
2587
+ "invalid_aggregate_order",
2588
+ 400
2589
+ );
2590
+ }
2591
+ }
2592
+ return {
2593
+ by,
2594
+ select,
2595
+ orderBy: spec.orderBy,
2596
+ desc: spec.desc,
2597
+ limit: spec.limit
2598
+ };
2599
+ }
2600
+ function aggregateValues(fn, values) {
2601
+ const present = values.filter((v) => v != null);
2602
+ switch (fn) {
2603
+ case "count":
2604
+ return values.length;
2605
+ case "sum":
2606
+ return present.reduce((s, v) => s + v, 0);
2607
+ case "avg":
2608
+ return present.length === 0 ? null : present.reduce((s, v) => s + v, 0) / present.length;
2609
+ case "countDistinct":
2610
+ return new Set(present).size;
2611
+ case "min":
2612
+ return present.length === 0 ? null : present.reduce((m, v) => v < m ? v : m);
2613
+ case "max":
2614
+ return present.length === 0 ? null : present.reduce((m, v) => v > m ? v : m);
2615
+ }
2616
+ }
2617
+ function compareForOrder(a, b) {
2618
+ if (a == null && b == null) return 0;
2619
+ if (a == null) return -1;
2620
+ if (b == null) return 1;
2621
+ if (a < b) return -1;
2622
+ if (a > b) return 1;
2623
+ return 0;
2624
+ }
2625
+ function computeAggregateInJs(rows, spec) {
2626
+ const groups = /* @__PURE__ */ new Map();
2627
+ for (const row of rows) {
2628
+ const key = JSON.stringify(spec.by.map((col) => row[col]));
2629
+ const group = groups.get(key);
2630
+ if (group) group.push(row);
2631
+ else groups.set(key, [row]);
2632
+ }
2633
+ if (spec.by.length === 0 && groups.size === 0) {
2634
+ groups.set("[]", []);
2635
+ }
2636
+ let out = Array.from(groups.values()).map((groupRows) => {
2637
+ const result = {};
2638
+ for (const col of spec.by) {
2639
+ result[col] = groupRows[0]?.[col];
2640
+ }
2641
+ for (const term of spec.select) {
2642
+ result[term.alias] = aggregateValues(
2643
+ term.fn,
2644
+ term.column != null ? groupRows.map((r) => r[term.column]) : groupRows
2645
+ );
2646
+ }
2647
+ return result;
2648
+ });
2649
+ if (spec.orderBy) {
2650
+ const key = spec.orderBy;
2651
+ out.sort((a, b) => compareForOrder(a[key], b[key]));
2652
+ if (spec.desc) out.reverse();
2653
+ }
2654
+ if (spec.limit != null) {
2655
+ out = out.slice(0, spec.limit);
2656
+ }
2657
+ return spec.by.length === 0 ? out[0] : out;
2658
+ }
2659
+ function deserializeAggregateRows(rows, spec, columns) {
2660
+ const schemaFor = /* @__PURE__ */ new Map();
2661
+ for (const col of spec.by) {
2662
+ schemaFor.set(
2663
+ col,
2664
+ columns.filter((c) => c.name === col)
2665
+ );
2666
+ }
2667
+ for (const term of spec.select) {
2668
+ if ((term.fn === "min" || term.fn === "max") && term.column != null) {
2669
+ schemaFor.set(
2670
+ term.alias,
2671
+ columns.filter((c) => c.name === term.column).map((c) => ({ ...c, name: term.alias }))
2672
+ );
2673
+ }
2674
+ }
2675
+ return rows.map((raw) => {
2676
+ const row = raw;
2677
+ const result = {};
2678
+ for (const [key, value] of Object.entries(row)) {
2679
+ const schema = schemaFor.get(key);
2680
+ if (schema) {
2681
+ result[key] = deserializeRow({ [key]: value }, schema)[key];
2682
+ } else {
2683
+ result[key] = value;
2684
+ }
2685
+ }
2686
+ return result;
2687
+ });
2688
+ }
2689
+ var IDENTIFIER_RE, AGGREGATE_FNS;
2690
+ var init_aggregate = __esm({
2691
+ "src/db/aggregate.ts"() {
2692
+ "use strict";
2693
+ init_errors();
2694
+ init_sql();
2695
+ IDENTIFIER_RE = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
2696
+ AGGREGATE_FNS = [
2697
+ "count",
2698
+ "sum",
2699
+ "avg",
2700
+ "min",
2701
+ "max",
2702
+ "countDistinct"
2703
+ ];
2704
+ }
2705
+ });
2706
+
2469
2707
  // src/db/query.ts
2470
2708
  function extractFieldName(accessor) {
2471
2709
  const source = accessor.toString();
@@ -2480,6 +2718,7 @@ var init_query = __esm({
2480
2718
  "use strict";
2481
2719
  init_predicate();
2482
2720
  init_sql();
2721
+ init_aggregate();
2483
2722
  Query = class _Query {
2484
2723
  _predicates;
2485
2724
  _sortAccessor;
@@ -2490,6 +2729,8 @@ var init_query = __esm({
2490
2729
  /** @internal Pre-compiled WHERE clause (bypasses predicate compiler). Used by Table.get(). */
2491
2730
  _rawWhere;
2492
2731
  _rawWhereParams;
2732
+ /** @internal See AggregateState — set by aggregate terminals. */
2733
+ _aggregate;
2493
2734
  /** @internal Post-process transform applied after row deserialization. */
2494
2735
  _postProcess;
2495
2736
  constructor(config, options) {
@@ -2502,6 +2743,7 @@ var init_query = __esm({
2502
2743
  this._postProcess = options?.postProcess;
2503
2744
  this._rawWhere = options?.rawWhere;
2504
2745
  this._rawWhereParams = options?.rawWhereParams;
2746
+ this._aggregate = options?.aggregate;
2505
2747
  }
2506
2748
  _clone(overrides) {
2507
2749
  return new _Query(this._config, {
@@ -2512,7 +2754,8 @@ var init_query = __esm({
2512
2754
  offset: overrides.offset ?? this._offset,
2513
2755
  postProcess: overrides.postProcess,
2514
2756
  rawWhere: this._rawWhere,
2515
- rawWhereParams: this._rawWhereParams
2757
+ rawWhereParams: this._rawWhereParams,
2758
+ aggregate: overrides.aggregate ?? this._aggregate
2516
2759
  });
2517
2760
  }
2518
2761
  filter(predicate, bindings) {
@@ -2550,9 +2793,60 @@ var init_query = __esm({
2550
2793
  }
2551
2794
  count() {
2552
2795
  return this._clone({
2796
+ aggregate: { kind: "scalar", fn: "count" },
2553
2797
  postProcess: (rows) => rows.length
2554
2798
  });
2555
2799
  }
2800
+ /**
2801
+ * Sum of a numeric field across matching rows. Compiles to a SQL
2802
+ * aggregate (TOTAL) when possible — no rows are fetched. Returns 0 for
2803
+ * an empty set; NULL values are skipped (SQL semantics).
2804
+ */
2805
+ sum(accessor) {
2806
+ return this._clone({
2807
+ aggregate: { kind: "scalar", fn: "sum", accessor },
2808
+ postProcess: (rows) => rows.reduce((total, row) => {
2809
+ const value = accessor(row);
2810
+ return value == null ? total : total + value;
2811
+ }, 0)
2812
+ });
2813
+ }
2814
+ /**
2815
+ * Average of a numeric field across matching rows. Compiles to SQL AVG
2816
+ * when possible. Returns null for an empty set (or when every value is
2817
+ * null) — SQL AVG semantics.
2818
+ */
2819
+ avg(accessor) {
2820
+ return this._clone({
2821
+ aggregate: { kind: "scalar", fn: "avg", accessor },
2822
+ postProcess: (rows) => {
2823
+ const values = rows.map((row) => accessor(row)).filter((v) => v != null);
2824
+ return values.length === 0 ? null : values.reduce((s, v) => s + v, 0) / values.length;
2825
+ }
2826
+ });
2827
+ }
2828
+ /**
2829
+ * Number of distinct non-null values of a field across matching rows.
2830
+ * Compiles to SQL COUNT(DISTINCT col) when possible.
2831
+ */
2832
+ countDistinct(accessor) {
2833
+ return this._clone({
2834
+ aggregate: { kind: "scalar", fn: "countDistinct", accessor },
2835
+ postProcess: (rows) => new Set(rows.map((row) => accessor(row)).filter((v) => v != null)).size
2836
+ });
2837
+ }
2838
+ aggregate(spec) {
2839
+ const normalized = normalizeAggregateSpec(
2840
+ spec
2841
+ );
2842
+ return this._clone({
2843
+ aggregate: { kind: "grouped", spec: normalized },
2844
+ postProcess: (rows) => computeAggregateInJs(
2845
+ rows,
2846
+ normalized
2847
+ )
2848
+ });
2849
+ }
2556
2850
  some() {
2557
2851
  return this._clone({
2558
2852
  limit: 1,
@@ -2623,6 +2917,10 @@ var init_query = __esm({
2623
2917
  const compiled = this._compilePredicates();
2624
2918
  const sortField = this._sortAccessor ? extractFieldName(this._sortAccessor) : void 0;
2625
2919
  if (compiled.allSql) {
2920
+ const agg = this._compileAggregateSql(compiled.sqlWhere);
2921
+ if (agg) {
2922
+ return { type: "query", query: agg.query, fallbackQuery: null, config: this._config, processRaw: agg.processRaw };
2923
+ }
2626
2924
  const query = buildSelect(this._config.tableName, {
2627
2925
  where: compiled.sqlWhere || void 0,
2628
2926
  orderBy: sortField ?? void 0,
@@ -2654,6 +2952,9 @@ var init_query = __esm({
2654
2952
  * For JS-fallback queries: filter, sort, and slice in JS.
2655
2953
  */
2656
2954
  static _processResults(result, compiled) {
2955
+ if (compiled.processRaw) {
2956
+ return compiled.processRaw(result);
2957
+ }
2657
2958
  const rows = result.rows.map(
2658
2959
  (row) => deserializeRow(
2659
2960
  row,
@@ -2686,10 +2987,81 @@ var init_query = __esm({
2686
2987
  // PromiseLike
2687
2988
  // -------------------------------------------------------------------------
2688
2989
  then(onfulfilled, onrejected) {
2689
- const promise = this._execute().then(
2690
- (rows) => this._postProcess ? this._postProcess(rows) : rows
2691
- );
2692
- return promise.then(onfulfilled, onrejected);
2990
+ return this._run().then(onfulfilled, onrejected);
2991
+ }
2992
+ async _run() {
2993
+ if (this._aggregate && !this._rawWhere) {
2994
+ const compiled = this._compilePredicates();
2995
+ if (compiled.allSql) {
2996
+ const agg = this._compileAggregateSql(compiled.sqlWhere);
2997
+ if (agg) {
2998
+ const results = await this._config.executeBatch([agg.query]);
2999
+ return agg.processRaw(results[0]);
3000
+ }
3001
+ }
3002
+ }
3003
+ const rows = await this._execute();
3004
+ return this._postProcess ? this._postProcess(rows) : rows;
3005
+ }
3006
+ /**
3007
+ * @internal Compile the pending aggregate into a single SQL statement +
3008
+ * result processor. Returns null (→ fetch path, postProcess computes the
3009
+ * aggregate) when there is no aggregate, when a pre-compiled WHERE is in
3010
+ * play (Table.get()), when limit/offset are set (aggregate-the-page
3011
+ * semantics — COUNT(*)/TOTAL can't express "over the limited page"), or
3012
+ * when a scalar accessor doesn't extract to a column (computed accessors
3013
+ * stay correct via JS, with a warning to prompt optimization).
3014
+ */
3015
+ _compileAggregateSql(sqlWhere) {
3016
+ const agg = this._aggregate;
3017
+ if (!agg || this._rawWhere || this._limit != null || this._offset != null) {
3018
+ return null;
3019
+ }
3020
+ const where = sqlWhere || void 0;
3021
+ if (agg.kind === "scalar") {
3022
+ let column = null;
3023
+ if (agg.accessor) {
3024
+ column = extractFieldName(agg.accessor);
3025
+ if (column == null) {
3026
+ console.warn(
3027
+ `[mindstudio] ${agg.fn}() accessor on '${this._config.tableName}' could not be compiled to SQL \u2014 fetching matching rows and computing in JS`
3028
+ );
3029
+ return null;
3030
+ }
3031
+ }
3032
+ const query2 = buildScalarAggregate(
3033
+ this._config.tableName,
3034
+ agg.fn,
3035
+ column,
3036
+ where
3037
+ );
3038
+ const processRaw2 = (result) => {
3039
+ const value = result.rows[0]?.value;
3040
+ if (agg.fn === "avg") {
3041
+ return value == null ? null : Number(value);
3042
+ }
3043
+ return Number(value ?? 0);
3044
+ };
3045
+ return { query: query2, processRaw: processRaw2 };
3046
+ }
3047
+ const spec = agg.spec;
3048
+ const query = buildGroupedAggregate(this._config.tableName, {
3049
+ by: spec.by,
3050
+ select: spec.select,
3051
+ where,
3052
+ orderBy: spec.orderBy,
3053
+ desc: spec.desc,
3054
+ limit: spec.limit
3055
+ });
3056
+ const processRaw = (result) => {
3057
+ const rows = deserializeAggregateRows(
3058
+ result.rows,
3059
+ spec,
3060
+ this._config.columns
3061
+ );
3062
+ return spec.by.length === 0 ? rows[0] : rows;
3063
+ };
3064
+ return { query, processRaw };
2693
3065
  }
2694
3066
  catch(onrejected) {
2695
3067
  return this.then(void 0, onrejected);
@@ -2931,7 +3303,34 @@ var init_table = __esm({
2931
3303
  max(accessor) {
2932
3304
  return this.sortBy(accessor).reverse().first();
2933
3305
  }
2934
- /** Group rows by a field. Returns a Map. */
3306
+ /**
3307
+ * Sum of a numeric field across all rows (0 for an empty table).
3308
+ * Runs as a SQL aggregate — no rows are fetched. Chain from filter()
3309
+ * to sum a subset: `Orders.filter(...).sum(o => o.amount)`.
3310
+ */
3311
+ sum(accessor) {
3312
+ return this.toArray().sum(accessor);
3313
+ }
3314
+ /**
3315
+ * Average of a numeric field across all rows (null for an empty table).
3316
+ * Runs as a SQL aggregate — no rows are fetched.
3317
+ */
3318
+ avg(accessor) {
3319
+ return this.toArray().avg(accessor);
3320
+ }
3321
+ /**
3322
+ * Number of distinct non-null values of a field across all rows.
3323
+ * Runs as a SQL aggregate — no rows are fetched.
3324
+ */
3325
+ countDistinct(accessor) {
3326
+ return this.toArray().countDistinct(accessor);
3327
+ }
3328
+ aggregate(spec) {
3329
+ return this.toArray().aggregate(spec);
3330
+ }
3331
+ /** Group rows by a field. Returns a Map of FULL rows per group — it
3332
+ * fetches everything. For grouped counts/sums/averages over tables that
3333
+ * can grow large, use `aggregate()` instead (SQL GROUP BY, no row fetch). */
2935
3334
  groupBy(accessor) {
2936
3335
  return new Query(this._config).groupBy(accessor);
2937
3336
  }
@@ -3169,6 +3568,64 @@ var init_table = __esm({
3169
3568
  }
3170
3569
  });
3171
3570
 
3571
+ // src/db/raw.ts
3572
+ var RawQuery;
3573
+ var init_raw = __esm({
3574
+ "src/db/raw.ts"() {
3575
+ "use strict";
3576
+ init_sql();
3577
+ RawQuery = class _RawQuery {
3578
+ _databaseId;
3579
+ _executeBatch;
3580
+ _query;
3581
+ constructor(databaseId, executeBatch, query) {
3582
+ this._databaseId = databaseId;
3583
+ this._executeBatch = executeBatch;
3584
+ this._query = query;
3585
+ }
3586
+ // -------------------------------------------------------------------------
3587
+ // PromiseLike — executes on await
3588
+ // -------------------------------------------------------------------------
3589
+ then(onfulfilled, onrejected) {
3590
+ return this._execute().then(onfulfilled, onrejected);
3591
+ }
3592
+ catch(onrejected) {
3593
+ return this._execute().catch(onrejected);
3594
+ }
3595
+ // -------------------------------------------------------------------------
3596
+ // Batch support
3597
+ // -------------------------------------------------------------------------
3598
+ /** @internal Compile for db.batch(). */
3599
+ _compile() {
3600
+ return {
3601
+ type: "raw",
3602
+ query: this._query,
3603
+ config: { databaseId: this._databaseId },
3604
+ processResult: (result) => _RawQuery._process(result)
3605
+ };
3606
+ }
3607
+ /** @internal Process a raw SQL result. Used by db.batch(). */
3608
+ static _processResults(result, compiled) {
3609
+ return compiled.processResult(result);
3610
+ }
3611
+ // -------------------------------------------------------------------------
3612
+ // Execution
3613
+ // -------------------------------------------------------------------------
3614
+ /** Schema-less deserialization: strips @@user@@ prefixes and best-effort
3615
+ * parses JSON-looking strings; makes no other representation promises. */
3616
+ static _process(result) {
3617
+ return result.rows.map(
3618
+ (row) => deserializeRow(row, [])
3619
+ );
3620
+ }
3621
+ async _execute() {
3622
+ const results = await this._executeBatch([this._query]);
3623
+ return _RawQuery._process(results[0]);
3624
+ }
3625
+ };
3626
+ }
3627
+ });
3628
+
3172
3629
  // src/db/index.ts
3173
3630
  function createDb(databases, executeBatch, authConfig, syncRoles) {
3174
3631
  return {
@@ -3196,6 +3653,16 @@ function createDb(databases, executeBatch, authConfig, syncRoles) {
3196
3653
  fromNow: (ms) => Date.now() + ms,
3197
3654
  // --- User references ---
3198
3655
  userRef: (id) => id.startsWith(USER_PREFIX) ? id.slice(USER_PREFIX.length) : id,
3656
+ // --- Raw SQL (read-only) ---
3657
+ sql(query, params, options) {
3658
+ validateRawSql(query);
3659
+ const database = resolveDatabase(databases, options?.database);
3660
+ return new RawQuery(
3661
+ database.id,
3662
+ (queries) => executeBatch(database.id, queries),
3663
+ { sql: query, params: params?.map(serializeParam) }
3664
+ );
3665
+ },
3199
3666
  // --- Batch execution ---
3200
3667
  batch: ((...operations) => {
3201
3668
  return (async () => {
@@ -3206,8 +3673,11 @@ function createDb(databases, executeBatch, authConfig, syncRoles) {
3206
3673
  if (op instanceof Mutation) {
3207
3674
  return op._compile();
3208
3675
  }
3676
+ if (op instanceof RawQuery) {
3677
+ return op._compile();
3678
+ }
3209
3679
  throw new MindStudioError(
3210
- "db.batch() only accepts Query and Mutation objects (from .filter(), .update(), .push(), etc.)",
3680
+ "db.batch() only accepts Query, Mutation, and RawQuery objects (from .filter(), .update(), .push(), db.sql(), etc.)",
3211
3681
  "invalid_batch_operation",
3212
3682
  400
3213
3683
  );
@@ -3220,6 +3690,8 @@ function createDb(databases, executeBatch, authConfig, syncRoles) {
3220
3690
  if (c.type === "query") {
3221
3691
  const sqlQuery = c.query ?? c.fallbackQuery;
3222
3692
  groups.get(dbId).push({ opIndex: i, sqlQueries: [sqlQuery] });
3693
+ } else if (c.type === "raw") {
3694
+ groups.get(dbId).push({ opIndex: i, sqlQueries: [c.query] });
3223
3695
  } else {
3224
3696
  groups.get(dbId).push({ opIndex: i, sqlQueries: c.queries });
3225
3697
  }
@@ -3252,6 +3724,8 @@ function createDb(databases, executeBatch, authConfig, syncRoles) {
3252
3724
  );
3253
3725
  }
3254
3726
  return Query._processResults(results[0], c);
3727
+ } else if (c.type === "raw") {
3728
+ return RawQuery._processResults(results[0], c);
3255
3729
  } else {
3256
3730
  return Mutation._processResults(results, c);
3257
3731
  }
@@ -3260,7 +3734,23 @@ function createDb(databases, executeBatch, authConfig, syncRoles) {
3260
3734
  })
3261
3735
  };
3262
3736
  }
3263
- function resolveTable(databases, tableName, databaseHint) {
3737
+ function validateRawSql(query) {
3738
+ if (typeof query !== "string" || !query.trim()) {
3739
+ throw new MindStudioError(
3740
+ "db.sql() requires a non-empty SQL string.",
3741
+ "invalid_sql",
3742
+ 400
3743
+ );
3744
+ }
3745
+ if (!/^\s*(select|with)\b/i.test(query)) {
3746
+ throw new MindStudioError(
3747
+ "db.sql() is read-only \u2014 the statement must start with SELECT or WITH. Use Table methods (push/update/upsert/remove) for writes.",
3748
+ "sql_read_only",
3749
+ 400
3750
+ );
3751
+ }
3752
+ }
3753
+ function resolveDatabase(databases, hint) {
3264
3754
  if (databases.length === 0) {
3265
3755
  throw new MindStudioError(
3266
3756
  `No databases found in app context. Make sure the app has at least one database configured.`,
@@ -3268,18 +3758,40 @@ function resolveTable(databases, tableName, databaseHint) {
3268
3758
  400
3269
3759
  );
3270
3760
  }
3271
- if (databaseHint) {
3761
+ if (hint) {
3272
3762
  const targetDb = databases.find(
3273
- (db) => db.id === databaseHint || db.name === databaseHint
3763
+ (db) => db.id === hint || db.name === hint
3274
3764
  );
3275
3765
  if (!targetDb) {
3276
3766
  const available = databases.map((db) => db.name || db.id).join(", ");
3277
3767
  throw new MindStudioError(
3278
- `Database "${databaseHint}" not found. Available databases: ${available}`,
3768
+ `Database "${hint}" not found. Available databases: ${available}`,
3279
3769
  "database_not_found",
3280
3770
  400
3281
3771
  );
3282
3772
  }
3773
+ return targetDb;
3774
+ }
3775
+ if (databases.length > 1) {
3776
+ const available = databases.map((db) => db.name || db.id).join(", ");
3777
+ throw new MindStudioError(
3778
+ `This app has multiple databases \u2014 pass { database } to pick one. Available databases: ${available}`,
3779
+ "ambiguous_database",
3780
+ 400
3781
+ );
3782
+ }
3783
+ return databases[0];
3784
+ }
3785
+ function resolveTable(databases, tableName, databaseHint) {
3786
+ if (databases.length === 0) {
3787
+ throw new MindStudioError(
3788
+ `No databases found in app context. Make sure the app has at least one database configured.`,
3789
+ "no_databases",
3790
+ 400
3791
+ );
3792
+ }
3793
+ if (databaseHint) {
3794
+ const targetDb = resolveDatabase(databases, databaseHint);
3283
3795
  const table = targetDb.tables.find((t) => t.name === tableName);
3284
3796
  if (!table) {
3285
3797
  const available = targetDb.tables.map((t) => t.name).join(", ");
@@ -3314,8 +3826,10 @@ var init_db = __esm({
3314
3826
  init_table();
3315
3827
  init_query();
3316
3828
  init_mutation();
3829
+ init_raw();
3317
3830
  init_sql();
3318
3831
  init_table();
3832
+ init_raw();
3319
3833
  }
3320
3834
  });
3321
3835
 
@@ -3692,6 +4206,43 @@ var init_voice = __esm({
3692
4206
  }
3693
4207
  });
3694
4208
 
4209
+ // src/analytics/index.ts
4210
+ function createAnalytics(call) {
4211
+ return {
4212
+ query(spec) {
4213
+ return call("query", spec);
4214
+ },
4215
+ live() {
4216
+ return call("live", {});
4217
+ },
4218
+ sources(options) {
4219
+ return call("sources", options ?? {});
4220
+ },
4221
+ map(options) {
4222
+ return call("map", options ?? {});
4223
+ },
4224
+ aiSources(options) {
4225
+ return call("ai-sources", options ?? {});
4226
+ },
4227
+ crawlers: {
4228
+ overview(options) {
4229
+ return call("crawlers-overview", options ?? {});
4230
+ },
4231
+ timeseries(options) {
4232
+ return call("crawlers-timeseries", options ?? {});
4233
+ },
4234
+ recent(options) {
4235
+ return call("crawlers-recent", options ?? {});
4236
+ }
4237
+ }
4238
+ };
4239
+ }
4240
+ var init_analytics = __esm({
4241
+ "src/analytics/index.ts"() {
4242
+ "use strict";
4243
+ }
4244
+ });
4245
+
3695
4246
  // src/task/schema.ts
3696
4247
  function describe(value) {
3697
4248
  if (value === void 0) return "undefined";
@@ -4921,9 +5472,6 @@ function applyStepMethods(AgentClass) {
4921
5472
  proto.postToZapier = function(step, options) {
4922
5473
  return this.executeStep("postToZapier", step, options);
4923
5474
  };
4924
- proto.queryAppDatabase = function(step, options) {
4925
- return this.executeStep("queryAppDatabase", step, options);
4926
- };
4927
5475
  proto.queryDataSource = function(step, options) {
4928
5476
  return this.executeStep("queryDataSource", step, options);
4929
5477
  };
@@ -5160,9 +5708,11 @@ var init_client = __esm({
5160
5708
  init_config();
5161
5709
  init_auth();
5162
5710
  init_db();
5711
+ init_sql();
5163
5712
  init_files();
5164
5713
  init_datasources();
5165
5714
  init_voice();
5715
+ init_analytics();
5166
5716
  await init_task();
5167
5717
  await init_local();
5168
5718
  init_schema();
@@ -5205,6 +5755,7 @@ var init_client = __esm({
5205
5755
  _files;
5206
5756
  _dataSources;
5207
5757
  _voice;
5758
+ _analytics;
5208
5759
  /** @internal Auth type — 'internal' for CALLBACK_TOKEN (managed mode), 'apiKey' otherwise. */
5209
5760
  _authType;
5210
5761
  /** @internal Usage source sent on step executions (from MINDSTUDIO_REQUEST_SOURCE).
@@ -6221,6 +6772,27 @@ var init_client = __esm({
6221
6772
  get files() {
6222
6773
  return this._files ??= createFiles(this._filesRequest.bind(this));
6223
6774
  }
6775
+ /**
6776
+ * The `analytics` namespace — read the app's own traffic + event analytics
6777
+ * (auto-tracked pageviews, `analytics.track()` events). One general
6778
+ * `query()` (metrics × dimensions × filters × time) plus `live()`,
6779
+ * `sources()`, `map()`, `aiSources()`, `crawlers.*`. See {@link Analytics}
6780
+ * for the lifetime-rollup vs 90-day-events split.
6781
+ *
6782
+ * @example
6783
+ * ```ts
6784
+ * const top = await agent.analytics.query({
6785
+ * metrics: ['pageviews', 'visitors'],
6786
+ * dimensions: ['path'],
6787
+ * dateRange: 'all',
6788
+ * });
6789
+ * ```
6790
+ */
6791
+ get analytics() {
6792
+ return this._analytics ??= createAnalytics(
6793
+ this._analyticsRequest.bind(this)
6794
+ );
6795
+ }
6224
6796
  /**
6225
6797
  * Jewel surfaces: arrival-shaped triggers (`propose`) and the app-native
6226
6798
  * approval queue (`queue.list` / `queue.resolve`). See {@link JewelsApi}.
@@ -6349,6 +6921,16 @@ var init_client = __esm({
6349
6921
  fallbackCode: "voice_error"
6350
6922
  });
6351
6923
  }
6924
+ /**
6925
+ * @internal Transport for the `analytics` namespace —
6926
+ * POST /_internal/v2/analytics/<op> with the raw hook token.
6927
+ */
6928
+ async _analyticsRequest(op, body) {
6929
+ return this._brokeredRequest("analytics", op, body, {
6930
+ fallbackMessage: "Analytics read failed",
6931
+ fallbackCode: "analytics_error"
6932
+ });
6933
+ }
6352
6934
  /**
6353
6935
  * @internal Shared shape for the brokered `/_internal/v2/<ns>/<op>` data
6354
6936
  * planes. Factored out rather than copied per namespace so error handling
@@ -6542,6 +7124,24 @@ var init_client = __esm({
6542
7124
  ago: (ms) => Date.now() - ms,
6543
7125
  fromNow: (ms) => Date.now() + ms,
6544
7126
  userRef: (id) => id.startsWith("@@user@@") ? id.slice("@@user@@".length) : id,
7127
+ // Raw SQL — validate synchronously; database resolution needs context,
7128
+ // so it happens inside the executeBatch closure at execution time
7129
+ // (the same deferral lazy defineTable uses).
7130
+ sql: ((query, params, options) => {
7131
+ validateRawSql(query);
7132
+ return new RawQuery(
7133
+ "",
7134
+ async (queries) => {
7135
+ await agent.ensureContext();
7136
+ const database = resolveDatabase(
7137
+ agent._getContext().databases,
7138
+ options?.database
7139
+ );
7140
+ return agent._executeDbBatch(database.id, queries);
7141
+ },
7142
+ { sql: query, params: params?.map(serializeParam) }
7143
+ );
7144
+ }),
6545
7145
  // Batch needs context — hydrate first, then delegate to real db
6546
7146
  batch: ((...queries) => {
6547
7147
  return (async () => {
@@ -7085,6 +7685,21 @@ function buildReferenceDocs(data) {
7085
7685
 
7086
7686
  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\`.
7087
7687
 
7688
+ Database aggregation: \`count()\`, \`sum(fn)\`, \`avg(fn)\`, \`countDistinct(fn)\`, and \`aggregate(spec)\` compile to SQL aggregates (COUNT/TOTAL/AVG/GROUP BY) \u2014 no rows are fetched, so they stay cheap on tables of any size. NEVER page a whole table into memory to compute a summary; aggregate in the database. Grouped example (the spec uses STRING column names, and the filter uses bindings so it compiles to SQL):
7689
+ \`\`\`typescript
7690
+ const top = await Answers
7691
+ .filter((a, $) => a.surveyId === $.surveyId, { surveyId }) // bindings: lifts closure var so filter compiles to SQL
7692
+ .aggregate({
7693
+ by: ['questionId', 'dimension'],
7694
+ select: { n: { count: true }, total: { sum: 'score' }, avgScore: { avg: 'score' }, respondents: { countDistinct: 'responseId' } },
7695
+ orderBy: 'total', desc: true, limit: 20,
7696
+ });
7697
+ // Array<{ questionId, dimension, n: number, total: number, avgScore: number | null, respondents: number }>
7698
+ \`\`\`
7699
+ Select terms: \`{ count: true }\`, \`{ sum: 'col' }\`, \`{ avg: 'col' }\`, \`{ min: 'col' }\`, \`{ max: 'col' }\`, \`{ countDistinct: 'col' }\`. Omit \`by\` for whole-set aggregates (returns a single object). Empty-set semantics: count/countDistinct 0, sum 0, avg/min/max null. \`Table.min/max(fn)\` still return the full ROW with the min/max value; \`groupBy(fn)\` returns full rows per group and fetches everything \u2014 prefer \`aggregate()\` for summaries.
7700
+
7701
+ Raw SQL escape hatch (\`db.sql\`): \`await db.sql<T>('SELECT ... FROM t WHERE x = ? ...', [val])\` runs read-only raw SQL against the app's own managed database \u2014 for joins, subqueries, and window functions the typed API can't express. SELECT/WITH only (writes throw \u2014 use Table methods). Lazy and batchable via \`db.batch()\`. LAST RESORT: prefer the typed API (including \`aggregate()\`) whenever it can express the query; raw rows may not exactly match the typed API's value representations.
7702
+
7088
7703
  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.
7089
7704
 
7090
7705
  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.
@@ -7097,6 +7712,8 @@ function buildReferenceDocs(data) {
7097
7712
 
7098
7713
  Native approval flows (\`mindstudio.jewels.queue\`): For \`approve\`-mode methods, build the review inbox in the app itself. \`queue.list({ methodId?, limit? })\` returns pending items ({ id, methodId, subject, proposed, reasoning, proposedAt, expiresAt }); \`queue.resolve(itemId, { action: 'approve' | 'dismiss', input? })\` resolves one. Approve APPLIES the target method as the current session user \u2014 the reviewer \u2014 so the effect belongs to the human who clicked and the target method's own auth checks gate who may approve; ALSO gate the wrapping list/resolve methods with the app's reviewer role (\`auth.requireRole\`). Pass \`input\` to apply an edited version of the proposal (the edit is captured with the pair \u2014 high-value signal). Dismissal records the rejection without running anything. Unresolved items expire at the method's attributionWindow. Backend only (hook token).
7099
7714
 
7715
+ Reading the app's own traffic (\`analytics\`): The platform auto-tracks pageviews (and custom events via the frontend's \`analytics.track()\`); the backend \`analytics\` namespace is the read side, so app code can correlate real traffic with its own data (lifetime views next to each blog post, "N people reading now"). One general read \u2014 \`analytics.query({ metrics, dimensions?, filters?, dateRange?, granularity?, orderBy?, limit? })\` \u2014 shaped like the Plausible query API: metrics from \`pageviews | visitors | visits | events\`, at most ONE entity dimension (path, referrerHost, country, deviceType, eventName, ...) OR \`'time'\` (+ \`granularity\`), filter triples \`[['is'|'is_not'|'contains', dimension, values]]\`, \`dateRange\` shorthand (\`'7d'\`, \`'all'\`, or \`[startISO, endISO]\`). Example \u2014 lifetime views per page: \`await analytics.query({ metrics: ['pageviews', 'visitors'], dimensions: ['path'], dateRange: 'all', limit: 10 })\` \u2192 \`results[].{ dimensions: { path }, metrics: { pageviews, visitors } }\`. IMPORTANT retention split: queries whose filters are all \`is\` and touch at most one dimension read a rollup kept forever (full history, exact uniques); cross-dimension / \`is_not\` / \`contains\` queries scan raw events retained 90 days \u2014 the server clamps the window and reports it (\`meta.source\`, \`meta.clamped\`); metrics a shape can't carry appear in \`meta.metricsOmitted\`, never as silent zeros. \`city\` filter values are plain city names (any country); with \`dateRange: 'all'\` the granularity is a minimum the server coarsens as history grows. Specials: \`analytics.live()\` (current visitor count + countries + sparkline), \`analytics.sources()\` (per-session first-source ranking, classified incl. AI assistants), \`analytics.map()\`, \`analytics.aiSources()\`, \`analytics.crawlers.overview()/timeseries()/recent()\`. Backend only (hook token); reads the LIVE app's real traffic even from a dev sandbox. Rate limited per app (120 reads/min).
7716
+
7100
7717
  Reporting bugs / ideas (\`reportIssue\`): For building an in-app "Report a bug" or feedback feature, use \`mindstudio.reportIssue({ title, body?, kind?, reporter? })\` (also importable directly as \`reportIssue\`). It files an issue into the app's issue tracker \u2014 visible to the app's team in the dashboard and pickup-able by the Remy agent. Backend only: wire it as frontend UI \u2192 app backend method \u2192 \`reportIssue\` (a browser can't call it; it uses the app's hook token, and the app id is derived server-side). \`kind\` is \`'bug'\` (default) or \`'idea'\`. \`reporter\` is a free-form display label (a name, email, or ticket id) \u2014 NOT an identity check and not tied to a user; pass the current user's email if you have it, or omit for anonymous. It returns the created issue \u2014 show \`result.number\` to confirm "Reported as #42". Rate limited per app (20/min): catch \`MindStudioError\` with \`code === 'rate_limited'\` and show a graceful "try again shortly" message, and debounce the submit button (every call creates a new issue \u2014 no dedupe).
7101
7718
 
7102
7719
  Table options: \`db.defineTable<T>(name, { unique, defaults })\`.
@@ -7438,7 +8055,7 @@ var llmsContent;
7438
8055
  var init_llms_content = __esm({
7439
8056
  "src/generated/llms-content.ts"() {
7440
8057
  "use strict";
7441
- 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, pass its public image URL to `changeProfilePicture`. 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, tools, an output contract, and a tool-use model id. The SDK runs a tool-use loop \u2014 calling the model, executing tools, feeding results back \u2014 until structured output is produced. Tools are SDK action names and/or your own app methods, written as `{ appMethod: "save-vendor", description: "when to use it" }`. App methods run as the user who invoked the method that started the task, with their roles, so the agent can read and write app state on their behalf. Prefer `outputSchema` for the output contract: plain JSON Schema (`type`/`properties`/`required`/`enum`/`items`; nullable via type arrays like `["string","null"]` \u2014 no `oneOf`/`$ref`/`nullable`). Output is validated every turn with automatic repair, `result.output` is fully typed from the schema (no generic argument, no `parsedSuccessfully` check), and the call throws `MindStudioError` code `task_output_schema_mismatch` if the agent cannot produce conforming output. Example: `runTask({ prompt: "Research this restaurant and save it", input: { name: "Tartine" }, tools: ["searchGoogle", "fetchUrl", { appMethod: "saveRestaurant", description: "Persist the researched restaurant." }], outputSchema: { type: "object", properties: { name: { type: "string" }, url: { type: ["string", "null"] } }, required: ["name"] }, model: yourModelId })`. The legacy `structuredOutputExample` option (an example object the model imitates) still works but is unvalidated \u2014 check `result.parsedSuccessfully` before using its output. Pass a current tool-use model id (call `listModels` or ask). Don\'t hardcode one.\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 nanodollars (1e-9 USD; divide by 1e9 for dollars, e.g. 40000000 = $0.04)\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 in nanodollars on success)\nresult.totalBillingCost; // number | undefined \u2014 nanodollars (divide by 1e9 for USD)\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- Pass imageUrl for a single image, or imageUrls for multiple images analyzed together in one request.\n- Most vision models (OpenAI, Grok, Gemini) accept multiple images in one request. Ideogram describe is single-image only.\n- Input: `{ prompt: string, imageUrl?: string, imageUrls?: 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\nLegacy v1 data sources. v2 apps should use `dataSources.defineDataSource()` from @mindstudio-ai/agent instead, which owns parsing, chunking, embedding and citations. Create 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\nLegacy v1 data sources. v2 apps should use `dataSources.defineDataSource()` from @mindstudio-ai/agent instead, which owns parsing, chunking, embedding and citations. Delete 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\nLegacy v1 data sources. v2 apps should use `dataSources.defineDataSource()` from @mindstudio-ai/agent instead, which owns parsing, chunking, embedding and citations. Delete 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- Optionally set `model` to a specific document-extraction model (`mistral-ocr-latest`, `llamaparse`, `google-document-ai`); omit to use the platform default.\n- Maximum file size is 50MB per URL.\n- Input: `{ url: string | string[], model?: string }`\n- Output: `{ text: string | string[] }`\n\n#### fetchDataSourceDocument\nLegacy v1 data sources. v2 apps should use `dataSources.defineDataSource()` from @mindstudio-ai/agent instead, which owns parsing, chunking, embedding and citations. Fetch 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#### generate3dModel\nGenerate a 3D model using a 3D generation model.\n- Text-to-3D models use the prompt field.\n- Image-to-3D and multi-view models take image URLs through the selected model\'s configuration inputs.\n- The output is standardized as a GLB URL plus optional FBX/OBJ/USDZ, thumbnail, texture maps, and provider task ID.\n- Input: `{ prompt?: string, intermediateAsset?: boolean, threeDModelOverride?: { model: string, config?: object } }`\n- Output: `{ prompt?: string, glbUrl: string, fbxUrl?: string, objUrl?: string, usdzUrl?: string, thumbnailUrl?: string, textureUrls?: object[], providerTaskId?: string, resolvedConfig?: object }`\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\nLegacy v1 data sources. v2 apps should use `dataSources.defineDataSource()` from @mindstudio-ai/agent instead, which owns parsing, chunking, embedding and citations. List 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\nLegacy v1 data sources. v2 apps should use `dataSources.defineDataSource()` from @mindstudio-ai/agent instead, which owns parsing, chunking, embedding and citations. Search 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 by default.\n- Uses WaveSpeed\'s Ideogram background removal model when type is "advanced".\n- Output is re-hosted on the CDN as a PNG with transparency.\n- Input: `{ type?: "standard" | "advanced", imageUrl: string, autoCrop?: boolean }`\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, "summary" returns a model-written summary and requires the "firecrawl" service.\n- Can optionally capture a screenshot of each page.\n- Handles bot protection automatically; no proxy or rendering configuration is needed.\n- Input: `{ url: string, service?: "default" | "firecrawl", autoEnhance?: boolean, outputFormat?: "text" | "json" | "html" | "summary", 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 }, error?: { code: string, message: string }, costUnits?: number } | { text: string, html: string, json?: object, screenshotUrl?: string, metadata?: { title: string, description: string, url: string, image: string }, error?: { code: string, message: string }, costUnits?: number }[], screenshot?: string | 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 }, error?: { code: string, message: string }, costUnits?: number } }`\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 }, error?: { code: string, message: string }, costUnits?: number } }`\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, scrolling it first so lazy-loaded content is included.\n- Works on sites behind bot protection (Cloudflare and similar).\n- Cookie-consent banners and similar overlays are suppressed automatically.\n- `width` and `height` are the exact output pixel dimensions.\n- You can add a delay or wait for a CSS selector before capturing. A selector that never appears fails the step rather than capturing early.\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- Set fetchTopN to also read the page content of the first N results in the same step, instead of following up with a separate Scrape URL step per result. Costs meaningfully more (each page is a full scrape), so ask for the fewest you need.\n- Also returns Google\'s "people also ask" and related searches when available, which are useful for choosing a follow-up query.\n- Input: `{ query: string, exportType: "text" | "json", countryCode?: string, languageCode?: string, dateRange?: "hour" | "day" | "week" | "month" | "year" | "any", numResults?: number, fetchTopN?: number }`\n- Output: `{ results: { title: string, description: string, url: string, content?: string }[], peopleAlsoAsk?: { question?: string, snippet?: string, link?: string }[], relatedSearches?: 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 one or more specific recipient email addresses directly. Allowed recipients depend on the sender: when the app sends from a domain it owns (a verified custom domain or its <slug>.madewithremy.com subdomain) any recipient is allowed; when it falls back to the shared Remy address, recipients must be verified app users or members of the app\'s organization. (v1 apps cannot use a direct "to" \u2014 they must resolve recipients via a connection.)\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- Use "cc" and "bcc" to add visible / hidden recipients (a string or an array). They are subject to the same recipient rules as "to".\n- "to" is optional only in the sense that recipients can come from elsewhere \u2014 omit it and supply "cc"/"bcc" for a hidden-list send, or omit all three and recipients are resolved from an OAuth connection. Naming nobody at all is an error.\n- Bcc-only sends are supported: with no "to" or "cc", the To: header is addressed to the app\'s own sender address (the standard "undisclosed recipients" pattern) so recipients can\'t see each other. The returned "recipients" reflects that auto-filled address.\n- Every recipient counts toward the app\'s daily outbound cap, including cc and bcc.\n- The sender defaults automatically: v2 apps send from the app\'s own identity \u2014 its verified custom domain if set, else its platform subdomain (noreply@<slug>.madewithremy.com), else the default Remy address.\n- Optionally set "from" to a custom handle, but ONLY if the app has a custom domain or subdomain: a bare handle ("support" \u2192 support@<app-domain>), a full "support@your-domain.com", or "Name <support@your-domain.com>". The domain must be one the app owns, or the step fails.\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- The body is interpreted automatically: if it already looks like HTML it is sent as HTML, otherwise it is rendered from Markdown. Every email is sent as multipart with a plain-text alternative auto-derived from the body (better deliverability).\n- Set bodyType to override interpretation: "html" (send as-is), "markdown" (render to HTML), or "text" (plain text only, no HTML part). Default is "auto".\n- Optionally set "text" to supply your own plain-text alternative instead of the auto-derived one.\n- Use "attachments" to attach files by URL. Each entry is a URL string, or an object { url, filename?, contentType? } to control the attachment\'s displayed filename and MIME type.\n- When generateHtml is enabled, the body text is converted to a styled HTML email using an AI model (implies an HTML body).\n- Set replyTo to control the Reply-To address for replies.\n- For threaded replies in a shared inbox, set inReplyTo (the Message-ID being replied to) and references (prior Message-IDs in the thread).\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[], cc?: string | string[], bcc?: 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 | { url: string, filename?: string, contentType?: string })[], from?: string, bodyType?: "auto" | "html" | "markdown" | "text", text?: string, replyTo?: string, inReplyTo?: string, references?: string[] }`\n- Output: `{ recipients: string[], cc: string[], bcc: string[], from: 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\nLegacy v1 data sources. v2 apps should use `dataSources.defineDataSource()` from @mindstudio-ai/agent instead, which owns parsing, chunking, embedding and citations. Upload 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### 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### 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### Meshy\n\n#### meshyAnimate\nApply a preset animation to a rigged 3D character model using Meshy.\n- Requires a rig_task_id from a previously completed Meshy rigging step.\n- Select an animation from Meshy\'s library of 600+ preset animations.\n- Only works with humanoid (bipedal) rigged characters.\n- Supports post-processing: FPS change (24/25/30/60), FBX-to-USDZ conversion, or armature extraction.\n- Animation categories: DailyActions, WalkAndRun, Fighting, Dancing, BodyMovements.\n- Input: `{ rigTaskId: string, actionId: number }`\n- Output: `{ glbUrl: string, fbxUrl?: string, objUrl?: string, usdzUrl?: string, thumbnailUrl?: string, textureUrls?: object[], animations?: { name: string, glbUrl?: string, fbxUrl?: string }[], providerTaskId?: string }`\n\n#### meshyImageTo3d\nGenerate a 3D model from one or more images using Meshy. Uses the multi-image-to-3D endpoint.\n- Accepts 1-4 image URLs. All images should depict the same object from different angles for best results.\n- By default generates with textures. Set shouldTexture to false for mesh-only output.\n- Uses should_remesh: false to preserve UV mapping integrity.\n- Input: `{ imageUrls: string[], shouldTexture?: boolean, topology?: string, targetPolycount?: number, symmetryMode?: string, poseMode?: string, textureImageUrl?: string }`\n- Output: `{ glbUrl: string, fbxUrl?: string, objUrl?: string, usdzUrl?: string, thumbnailUrl?: string, textureUrls?: object[], animations?: { name: string, glbUrl?: string, fbxUrl?: string }[], providerTaskId?: string }`\n\n#### meshyRemesh\nRemesh an existing 3D model to adjust topology, polygon count, or convert formats using Meshy.\n- Provide either an input task ID (from a previous Meshy step) or a model URL.\n- Defaults to triangle topology with 30,000 target polys.\n- Useful for reducing face count before rigging (max 300k faces for rigging).\n- Input: `{ inputTaskId?: string, modelUrl?: string, topology?: string, targetPolycount?: number, resizeHeight?: number }`\n- Output: `{ glbUrl: string, fbxUrl?: string, objUrl?: string, usdzUrl?: string, thumbnailUrl?: string, textureUrls?: object[], animations?: { name: string, glbUrl?: string, fbxUrl?: string }[], providerTaskId?: string }`\n\n#### meshyRig\nAuto-rig a humanoid 3D model and generate basic walking/running animations using Meshy.\n- Only works well with standard humanoid (bipedal) models with clearly defined limbs.\n- Prefers model_url over input_task_id for cleaner rigging input.\n- Models with more than 300,000 faces should be remeshed first.\n- Returns rigged model files and optional basic animations.\n- Input: `{ inputTaskId?: string, modelUrl?: string, heightMeters?: number }`\n- Output: `{ glbUrl: string, fbxUrl?: string, objUrl?: string, usdzUrl?: string, thumbnailUrl?: string, textureUrls?: object[], animations?: { name: string, glbUrl?: string, fbxUrl?: string }[], providerTaskId?: string }`\n\n#### meshyTextTo3d\nGenerate a 3D model preview from a text prompt using Meshy. Produces an untextured mesh (preview stage).\n- Creates a text-to-3D preview task (mesh generation only, no texture).\n- Use the Meshy Texture step to apply textures to the preview.\n- Maximum prompt length is 600 characters.\n- Input: `{ prompt: string, modelType?: string, topology?: string, targetPolycount?: number, symmetryMode?: string, poseMode?: string }`\n- Output: `{ glbUrl: string, fbxUrl?: string, objUrl?: string, usdzUrl?: string, thumbnailUrl?: string, textureUrls?: object[], animations?: { name: string, glbUrl?: string, fbxUrl?: string }[], providerTaskId?: string }`\n\n#### meshyTexture\nApply or replace textures on a 3D model using a text prompt or reference image via Meshy.\n- Provide either an input task ID (from a previous Meshy step) or a model URL.\n- Provide either a text style prompt or an image style URL to guide texturing.\n- Supports .glb, .gltf, .obj, .fbx, .stl model formats when using modelUrl.\n- By default preserves original UVs (enableOriginalUv = true).\n- Works with any model source: text-to-3D previews, image-to-3D, remeshed models, or external files.\n- Input: `{ inputTaskId?: string, modelUrl?: string, textStylePrompt?: string, imageStyleUrl?: string, enableOriginalUv?: boolean, enablePbr?: boolean }`\n- Output: `{ glbUrl: string, fbxUrl?: string, objUrl?: string, usdzUrl?: string, thumbnailUrl?: string, textureUrls?: object[], animations?: { name: string, glbUrl?: string, fbxUrl?: string }[], providerTaskId?: string }`\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### Particle Podcasts\n\n#### particlePodcastsFindMentions\nFind every dialogue line mentioning a specific entity or company across all podcasts.\n- Provide `entityId` (for people, products, places) OR `companyId` (for organizations). At least one is required.\n- Use `contextLines` to include surrounding dialogue with each mention (default behavior is set by Particle).\n- Resolve a name to an `entityId` / `companyId` first via Search Companies (or by inspecting a Search Dialogue response).\n- Cursor-paginated; expect potentially large result sets for popular entities.\n- Input: `{ entityId?: string, companyId?: string, contextLines?: number, limit?: number, cursor?: string }`\n- Output: `unknown`\n\n#### particlePodcastsGetEpisode\nFetch full metadata for a single episode: details, speakers, entities, clips, and ads \u2014 merged into one response.\n- Pass an episode ID or slug as `id`.\n- Returns five sub-resources merged: `episode` (metadata), `speakers` (diarized speaker list), `entities` (knowledge-graph mentions), `clips` (AI-extracted highlights), `ads` (detected ad spots).\n- Use Get Episode Transcript separately when you need the full transcript text \u2014 it isn\'t bundled here because the payload is large and has its own format/range options.\n- Bills as 5 units against the get-episode event type (one per sub-call).\n- Input: `{ id: string }`\n- Output: `unknown`\n\n#### particlePodcastsGetEpisodeTranscript\nFetch the diarized transcript for an episode in dialogue, plain text, or SRT subtitle format.\n- Pass an episode ID or slug as `id`.\n- Use `format` = "dialogue" (default, with speaker turns), "text" (plain), or "srt" (subtitle).\n- Filter to a single speaker with `speaker`, or to a time range with `start` / `end` (seconds).\n- Transcripts are large \u2014 prefer time-range filtering when you only need a snippet.\n- Input: `{ id: string, format?: "dialogue" | "text" | "srt", speaker?: string, start?: number, end?: number }`\n- Output: `unknown`\n\n#### particlePodcastsSearchCompanies\nSearch the Particle knowledge graph for companies by name, ticker, domain, CIK, or QID.\n- Provide one or more identifiers: `q` (free-text name), `ticker` (e.g. "TSLA"), `domain` (e.g. "tesla.com"), `cik` (SEC), or `qid` (Wikidata).\n- Use this to resolve a company name to a canonical `companyId` for use with Find Mentions or Search Dialogue.\n- Returned company objects include slugs, domains, and IDs \u2014 any of these can be passed to downstream blocks.\n- Input: `{ q?: string, ticker?: string, domain?: string, cik?: string, qid?: string, entityId?: string, updatedAfter?: string, limit?: number, cursor?: string }`\n- Output: `unknown`\n\n#### particlePodcastsSearchDialogue\nSearch across podcast dialogue using semantic or keyword search. Returns matched lines grouped by episode.\n- Provide `semanticSearch` for meaning-based discovery ("find moments where someone talks about market timing") or `keywordSearch` for exact phrase/proper-noun matching. At least one must be provided.\n- Filter to a specific entity or company by passing `entityId` / `companyId`.\n- Each returned dialogue line carries the source episode + speaker so you can chain to Get Episode or Get Episode Transcript for context.\n- Cursor-paginated.\n- Input: `{ semanticSearch?: string, keywordSearch?: string, entityId?: string, companyId?: string, limit?: number, cursor?: string }`\n- Output: `unknown`\n\n#### particlePodcastsSearchPodcasts\nSearch and list podcasts in the Particle catalog by keyword, topic, or language.\n- Use `q` for free-text keyword search across podcast titles and descriptions.\n- Use `topic` to filter to a Particle taxonomy topic.\n- Use `language` (BCP 47, e.g. "en") to restrict to a language.\n- Returns podcast objects with canonical IDs and slugs. Pass either to other Particle Podcasts blocks.\n- Cursor-paginated; pass the returned `cursor` back to `cursor` for the next page.\n- Input: `{ q?: string, topic?: string, language?: string, suitabilityTier?: string, limit?: number, cursor?: string }`\n- Output: `unknown`\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### You.com\n\n#### youDotComFinanceResearch\nAsk a financial research question using You.com Finance Research and return the sourced response.\n- Use this for financial questions such as company analysis, earnings, market research, filings, macroeconomics, and due diligence.\n- researchEffort supports deep (default) or exhaustive.\n- Finance Research returns the same response shape as Web Research, but searches a finance-optimized index.\n- Use it for cited synthesis, not raw price feeds or structured time-series exports.\n- Input: `{ input: string, researchEffort?: "deep" | "exhaustive" }`\n- Output: `{ data: object | unknown[] }`\n\n#### youDotComGetPageContent\nFetch clean Markdown, HTML, or metadata for known URLs using the You.com Contents API.\n- Use this step when you already know the URLs. Use Web Search with livecrawl when You.com should discover pages from a query.\n- A single request supports up to 10 URLs.\n- Request only the formats you need. Markdown is recommended for LLM consumption.\n- Increase crawlTimeout for JavaScript-heavy pages, up to 60 seconds.\n- Individual pages can partially fail; check each returned item before processing.\n- Input: `{ urls: string[], formats?: ("markdown" | "html" | "metadata")[], crawlTimeout?: number }`\n- Output: `{ data: object | unknown[] }`\n\n#### youDotComLiveNews\nFetch live news articles through the You.com Search API and return the full structured response.\n- Defaults freshness to day for breaking or recent news.\n- Use country and language together to monitor regional or non-English news.\n- Use livecrawl: \'news\' with livecrawlFormats: [\'markdown\'] when you need full article text.\n- Use a custom freshness range like YYYY-MM-DDtoYYYY-MM-DD for historical news windows.\n- Input: `{ query: string, freshness?: string, count?: number, country?: string, language?: string, safesearch?: "off" | "moderate" | "strict", livecrawl?: "news" | "all", livecrawlFormats?: ("markdown" | "html")[] }`\n- Output: `{ data: object | unknown[] }`\n\n#### youDotComWebResearch\nAsk a research question and return a grounded You.com Research API answer with sources.\n- Use Web Search when you need raw URLs and snippets. Use Web Research when you want a synthesized answer with citations.\n- researchEffort controls depth and latency: lite, standard, deep, or exhaustive. standard is a good default.\n- sourceControl can restrict, exclude, or boost domains, and can apply freshness or country filters.\n- includeDomains cannot be combined with excludeDomains or boostDomains.\n- outputSchema returns structured output.content and is supported by standard, deep, and exhaustive, not lite.\n- Input: `{ input: string, researchEffort?: "lite" | "standard" | "deep" | "exhaustive", sourceControl?: { includeDomains?: string[], excludeDomains?: string[], boostDomains?: string[], freshness?: string, country?: string } | string, outputSchema?: object | string }`\n- Output: `{ data: object | unknown[] }`\n\n#### youDotComWebSearch\nSearch the web and news using the You.com Search API and return the full structured response.\n- Query supports You.com search operators:\n| Operator | Description | Example |\n| -------- | ----------- | ------- |\n| site: | Search within a domain and its subdomains | site:uscourts.gov |\n| filetype: | Search for a specific file type | filetype:pdf |\n| + | Require the exact term after the operator | +GAAP |\n| - | Exclude the exact term after the operator | -prs |\n| AND | Require both expressions | guitar AND Fender |\n| OR | Match either expression | guitar OR drum |\n| NOT | Negate an expression | NOT site:uscourts.gov |\n- Use livecrawl with livecrawlFormats: [\'markdown\'] when you need full page content instead of snippets.\n- Use the Get Page Content step when you already know the URLs to fetch.\n- Use freshness for recency: day, week, month, year, or YYYY-MM-DDtoYYYY-MM-DD.\n- Use country (ISO 3166-1 alpha-2) and language (BCP 47) to focus results geographically or linguistically.\n- includeDomains cannot be combined with excludeDomains or boostDomains.\n- Input: `{ query: string, count?: number, freshness?: string, country?: string, language?: string, offset?: number, safesearch?: "off" | "moderate" | "strict", livecrawl?: "web" | "news" | "all", livecrawlFormats?: ("markdown" | "html")[], crawlTimeout?: number, includeDomains?: string[], excludeDomains?: string[], boostDomains?: string[] }`\n- Output: `{ data: object | unknown[] }`\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 nanodollars (1e-9 USD)\n unitType: string; // What constitutes a unit (e.g. "token", "request")\n estimatedCost?: number; // Estimated total cost in nanodollars, 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)`\n**Deprecated for app file storage** \u2014 use the `files` store below (`files.defineStore(...).put(...)`): private by default, app-scoped, and served on the app\'s own domain. `uploadFile` uploads to the shared account media CDN and remains only for account-level assets (e.g. an agent avatar for `changeProfilePicture`). 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\n#### `files` \u2014 per-app file storage (private by default)\nTyped per-app file storage: user uploads, generated documents, images, marketing assets. Think of a store as a CDN-backed bucket the app talks to \u2014 NOT app-defined state like a `db` table. The API is *shaped* like the `db` namespace (define a store at module scope, then read/write), but its contents aren\'t modeled by code, and one store is shared across dev and prod (a file uploaded in the dev editor is already live in prod at the same URL). Creates are safe by default because keys default to unique (UUID, or a content-addressed hash); the operations to treat with care are `delete(key)` and overwriting a fixed key. Public objects are CDN-cached per their `Cache-Control`, set at put time via `cacheControl`: auto-minted keys default to immutable (cache-forever \u2014 the key is never reused), and named keys default to `public, max-age=300`, so overwriting one propagates publicly within ~5 minutes; pass `cacheControl` to tune (e.g. `\'public, max-age=60\'` for near-live config, `\'no-store\'` to revalidate every read). Files are served on the app\'s own domain: private stores (the default) are signed / session-authorized; public stores are world-readable, CDN-served, and images resize via query params (e.g. `?w=400&h=300&fit=cover`). Prefer this over the deprecated `uploadFile()` for app storage.\n\n```typescript\nimport { files } from \'@mindstudio-ai/agent\';\n\n// Define stores at module scope (access is pinned here).\nexport const Uploads = files.defineStore(\'uploads\'); // private (default)\nexport const Assets = files.defineStore(\'assets\', { access: \'public\' });\n\n// Store bytes and get a ready-to-use URL.\nconst file = await Uploads.put(buffer, { contentType: \'application/pdf\', filename: \'report.pdf\' });\nfile.url; // stable on-domain URL for a logged-in user (drop into <img>/<a download>)\nawait file.shareUrl(); // absolute signed URL that works with NO session (email/embed); default 24h\n```\n\nStore methods:\n- `put(content, { key?, contentType?, filename?, contentAddressed?, cacheControl? })` \u2192 `StoredFile`. `content` is `Buffer | Uint8Array | string`. Omit `key` for a random UUID; `contentAddressed: true` derives an immutable `<sha256>.<ext>` key. `cacheControl` sets the CDN caching of a public object (defaults: auto-minted keys immutable, named keys `public, max-age=300`).\n- `get(key)` \u2192 `Buffer` \xB7 `head(key)` \u2192 `StoredFile` \xB7 `exists(key)` \u2192 `boolean`\n- `list({ prefix?, cursor?, limit? })` \u2192 `{ files: StoredFile[]; cursor? }`\n- `delete(key)` \u2192 `void`\n- `shareUrl(key, { expiresIn? })` \u2192 signed absolute URL (private stores; no session needed)\n\n`StoredFile`: `{ store, key, access, size?, contentType?, updatedAt?, url, shareUrl() }`.\n\n`defineStore(name, options?)` options: `access: \'public\' | \'private\'` (default private), `maxSize` (bytes; cap for client-direct uploads), `contentTypes` (allowlist for client-direct uploads).\n\nUser uploads (client-direct \u2014 bytes go straight to storage, never through the app backend): a backend method mints a token, the frontend submits it with `@mindstudio-ai/interface`.\n```typescript\n// backend method\nconst token = await Uploads.createUploadToken({ contentType, maxSize: 25 * 1024 * 1024 }); // also accepts cacheControl\nreturn token;\n// frontend (@mindstudio-ai/interface)\nimport { platform } from \'@mindstudio-ai/interface\';\nconst { url } = await platform.upload(token, file, { onProgress });\n```\n\nGenerated assets: any action that produces a file (`generateImage`, `generateVideo`, `generateSpeech`, `generatePdf`, `upscaleImage`, \u2026) can put its output straight into one of these stores \u2014 pass the store handle as `store` in the OPTIONS object (second argument). Optional: omit it and the asset goes to the shared MindStudio CDN as before. Reach for it when the asset belongs to the app \u2014 private to a user, or listed alongside its other files.\n\n```typescript\nconst { imageUrl } = await mindstudio.generateImage({ prompt: \'a red sneaker\' }, { store: Assets });\n```\n\n#### `dataSources` \u2014 searchable document corpora (RAG)\n\nDeclare a data source at module scope and import the handle. The platform owns parsing, chunking, embedding, storage and tenant isolation; every hit returns a citation.\n```typescript\nimport { dataSources } from \'@mindstudio-ai/agent\';\nexport const Policies = dataSources.defineDataSource(\'policies\');\n\n// search\nconst { results } = await Policies.search(\'what are the payment terms?\', { topK: 5 });\n// options: topK (default 5, max 50), scoreThreshold, filter, mode, maxPerDocument, highlight, rerank, hybrid\nconst context = results.map((r) => r.text).join(String.fromCharCode(10, 10));\n\n// narrow before ranking: metadata tags, filename, pages, required words\nawait Policies.search(\'termination clause\', {\n filter: { metadata: { department: \'legal\', year: [2025, 2026] }, phrase: \'notice period\' },\n maxPerDocument: 2, // stop one document monopolizing the results\n});\n\n// mode: \'hybrid\' (default) | \'semantic\' | \'lexical\'. Lexical skips the query\n// embedding \u2014 fastest, right for identifier-shaped queries (error codes, SKUs).\nawait Policies.search(\'ERR-7741X\', { mode: \'lexical\' });\n\n// add a document (background \u2014 poll documents() for status)\nawait Policies.add(buffer, { filename: \'policy.pdf\', contentType: \'application/pdf\', metadata: { department: \'legal\' } });\nconst docs = await Policies.documents();\n\n// introspection \u2014 what is in the corpus, and how one document was split\nconst { chunkCount, pipeline } = await Policies.stats(); // counts + the config actually in effect\nconst chunks = await Policies.chunks(docs[0].id); // why a document does or doesn\'t match\n```\n\n`SearchHit`: `{ score, text, citation: { documentId, filename, pageNumber, chunkIndex, headingPath, boundingBox?, url } }`. `citation.url` is a stable on-domain link to the source document \u2014 drop it in an `<a href>`. `(documentId, chunkIndex)` is a chunk\'s stable identity \u2014 key an eval set on that, not on `text`.\n\nAlso on each hit: `retrievalRank`/`retrievalScore` (position before reranking \u2014 compare with the final position to see what reranking did). Three opt-in search options for debugging and display: `explain: true` adds `explain.{dense,lexical,matchedVia}` so you can see which half of hybrid found a hit; `expand: 1` adds `neighbors.{before,after}` for surrounding context; `highlight: true` adds `matches` \u2014 `{start, end}` offsets of query terms in `text`, for rendering highlights. None of them changes the results or their order.\n\n`filter` fields (all AND together, and only narrow): `metadata` (per-key equals, or any-of via an array), `filename`, `documentIds`, `pages: {min?, max?}`, `contains` (all words, any order), `phrase` (exact adjacent sequence). Document metadata is set at add time: scalars only, \u226416 keys, keys `[a-zA-Z0-9_-]`. Re-adding the same bytes with different metadata updates the tags in place \u2014 no re-processing.\n\nSearch is deterministic for a fixed corpus and configuration \u2014 same query, same order \u2014 so regression checks are meaningful. `search()` also returns `latencyMs`.\n\nOther methods: `stats()`, `documents()`, `chunks(documentId, {vectors?})`, `remove(documentId)`, `ensure(name?)` (rarely needed \u2014 `add` and `search` create-on-reference), and `DataSource.contentHash(bytes)` to check for an existing document before adding.\n\nSupported: pdf, docx, pptx, xlsx, odt, rtf, epub, images, txt, md, json, csv, tsv, log, html.\n\nAdding the same bytes twice is free \u2014 documents are content-addressed, so a re-add is a no-op when this source has already processed those exact bytes under its current configuration. Only reconfiguring the source makes a re-add do work again, and that is an explicit owner-triggered migration, never a side effect of a deploy.\n\nEverything in `SearchOptions` \u2014 `mode`, `filter`, `rerank`, `hybrid`, `maxPerDocument`, `highlight` \u2014 is free to change and takes effect on the next search. How documents are chunked and embedded is configured on the corpus itself with `mindstudio-prod datasources config` \u2014 not in code \u2014 and changing it on a populated source is rejected rather than silently rebuilding.\n\n**A data source is live and shared.** Unlike database tables there is no dev copy and no per-release isolation: adding or removing a document changes what the deployed app retrieves, immediately. Scenarios never reset a data source, and re-ingesting a large corpus costs real money.\n';
8058
+ 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, pass its public image URL to `changeProfilePicture`. 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, tools, an output contract, and a tool-use model id. The SDK runs a tool-use loop \u2014 calling the model, executing tools, feeding results back \u2014 until structured output is produced. Tools are SDK action names and/or your own app methods, written as `{ appMethod: "save-vendor", description: "when to use it" }`. App methods run as the user who invoked the method that started the task, with their roles, so the agent can read and write app state on their behalf. Prefer `outputSchema` for the output contract: plain JSON Schema (`type`/`properties`/`required`/`enum`/`items`; nullable via type arrays like `["string","null"]` \u2014 no `oneOf`/`$ref`/`nullable`). Output is validated every turn with automatic repair, `result.output` is fully typed from the schema (no generic argument, no `parsedSuccessfully` check), and the call throws `MindStudioError` code `task_output_schema_mismatch` if the agent cannot produce conforming output. Example: `runTask({ prompt: "Research this restaurant and save it", input: { name: "Tartine" }, tools: ["searchGoogle", "fetchUrl", { appMethod: "saveRestaurant", description: "Persist the researched restaurant." }], outputSchema: { type: "object", properties: { name: { type: "string" }, url: { type: ["string", "null"] } }, required: ["name"] }, model: yourModelId })`. The legacy `structuredOutputExample` option (an example object the model imitates) still works but is unvalidated \u2014 check `result.parsedSuccessfully` before using its output. Pass a current tool-use model id (call `listModels` or ask). Don\'t hardcode one.\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 nanodollars (1e-9 USD; divide by 1e9 for dollars, e.g. 40000000 = $0.04)\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 in nanodollars on success)\nresult.totalBillingCost; // number | undefined \u2014 nanodollars (divide by 1e9 for USD)\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- Pass imageUrl for a single image, or imageUrls for multiple images analyzed together in one request.\n- Most vision models (OpenAI, Grok, Gemini) accept multiple images in one request. Ideogram describe is single-image only.\n- Input: `{ prompt: string, imageUrl?: string, imageUrls?: 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\nLegacy v1 data sources. v2 apps should use `dataSources.defineDataSource()` from @mindstudio-ai/agent instead, which owns parsing, chunking, embedding and citations. Create 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\nLegacy v1 data sources. v2 apps should use `dataSources.defineDataSource()` from @mindstudio-ai/agent instead, which owns parsing, chunking, embedding and citations. Delete 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\nLegacy v1 data sources. v2 apps should use `dataSources.defineDataSource()` from @mindstudio-ai/agent instead, which owns parsing, chunking, embedding and citations. Delete 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- Optionally set `model` to a specific document-extraction model (`mistral-ocr-latest`, `llamaparse`, `google-document-ai`); omit to use the platform default.\n- Maximum file size is 50MB per URL.\n- Input: `{ url: string | string[], model?: string }`\n- Output: `{ text: string | string[] }`\n\n#### fetchDataSourceDocument\nLegacy v1 data sources. v2 apps should use `dataSources.defineDataSource()` from @mindstudio-ai/agent instead, which owns parsing, chunking, embedding and citations. Fetch 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#### generate3dModel\nGenerate a 3D model using a 3D generation model.\n- Text-to-3D models use the prompt field.\n- Image-to-3D and multi-view models take image URLs through the selected model\'s configuration inputs.\n- The output is standardized as a GLB URL plus optional FBX/OBJ/USDZ, thumbnail, texture maps, and provider task ID.\n- Input: `{ prompt?: string, intermediateAsset?: boolean, threeDModelOverride?: { model: string, config?: object } }`\n- Output: `{ prompt?: string, glbUrl: string, fbxUrl?: string, objUrl?: string, usdzUrl?: string, thumbnailUrl?: string, textureUrls?: object[], providerTaskId?: string, resolvedConfig?: object }`\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\nLegacy v1 data sources. v2 apps should use `dataSources.defineDataSource()` from @mindstudio-ai/agent instead, which owns parsing, chunking, embedding and citations. List 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#### queryDataSource\nLegacy v1 data sources. v2 apps should use `dataSources.defineDataSource()` from @mindstudio-ai/agent instead, which owns parsing, chunking, embedding and citations. Search 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 by default.\n- Uses WaveSpeed\'s Ideogram background removal model when type is "advanced".\n- Output is re-hosted on the CDN as a PNG with transparency.\n- Input: `{ type?: "standard" | "advanced", imageUrl: string, autoCrop?: boolean }`\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, "summary" returns a model-written summary and requires the "firecrawl" service.\n- Can optionally capture a screenshot of each page.\n- Handles bot protection automatically; no proxy or rendering configuration is needed.\n- Input: `{ url: string, service?: "default" | "firecrawl", autoEnhance?: boolean, outputFormat?: "text" | "json" | "html" | "summary", 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 }, error?: { code: string, message: string }, costUnits?: number } | { text: string, html: string, json?: object, screenshotUrl?: string, metadata?: { title: string, description: string, url: string, image: string }, error?: { code: string, message: string }, costUnits?: number }[], screenshot?: string | 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 }, error?: { code: string, message: string }, costUnits?: number } }`\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 }, error?: { code: string, message: string }, costUnits?: number } }`\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, scrolling it first so lazy-loaded content is included.\n- Works on sites behind bot protection (Cloudflare and similar).\n- Cookie-consent banners and similar overlays are suppressed automatically.\n- `width` and `height` are the exact output pixel dimensions.\n- You can add a delay or wait for a CSS selector before capturing. A selector that never appears fails the step rather than capturing early.\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- Set fetchTopN to also read the page content of the first N results in the same step, instead of following up with a separate Scrape URL step per result. Costs meaningfully more (each page is a full scrape), so ask for the fewest you need.\n- Also returns Google\'s "people also ask" and related searches when available, which are useful for choosing a follow-up query.\n- Input: `{ query: string, exportType: "text" | "json", countryCode?: string, languageCode?: string, dateRange?: "hour" | "day" | "week" | "month" | "year" | "any", numResults?: number, fetchTopN?: number }`\n- Output: `{ results: { title: string, description: string, url: string, content?: string }[], peopleAlsoAsk?: { question?: string, snippet?: string, link?: string }[], relatedSearches?: 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 one or more specific recipient email addresses directly. Allowed recipients depend on the sender: when the app sends from a domain it owns (a verified custom domain or its <slug>.madewithremy.com subdomain) any recipient is allowed; when it falls back to the shared Remy address, recipients must be verified app users or members of the app\'s organization. (v1 apps cannot use a direct "to" \u2014 they must resolve recipients via a connection.)\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- Use "cc" and "bcc" to add visible / hidden recipients (a string or an array). They are subject to the same recipient rules as "to".\n- "to" is optional only in the sense that recipients can come from elsewhere \u2014 omit it and supply "cc"/"bcc" for a hidden-list send, or omit all three and recipients are resolved from an OAuth connection. Naming nobody at all is an error.\n- Bcc-only sends are supported: with no "to" or "cc", the To: header is addressed to the app\'s own sender address (the standard "undisclosed recipients" pattern) so recipients can\'t see each other. The returned "recipients" reflects that auto-filled address.\n- Every recipient counts toward the app\'s daily outbound cap, including cc and bcc.\n- The sender defaults automatically: v2 apps send from the app\'s own identity \u2014 its verified custom domain if set, else its platform subdomain (noreply@<slug>.madewithremy.com), else the default Remy address.\n- Optionally set "from" to a custom handle, but ONLY if the app has a custom domain or subdomain: a bare handle ("support" \u2192 support@<app-domain>), a full "support@your-domain.com", or "Name <support@your-domain.com>". The domain must be one the app owns, or the step fails.\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- The body is interpreted automatically: if it already looks like HTML it is sent as HTML, otherwise it is rendered from Markdown. Every email is sent as multipart with a plain-text alternative auto-derived from the body (better deliverability).\n- Set bodyType to override interpretation: "html" (send as-is), "markdown" (render to HTML), or "text" (plain text only, no HTML part). Default is "auto".\n- Optionally set "text" to supply your own plain-text alternative instead of the auto-derived one.\n- Use "attachments" to attach files by URL. Each entry is a URL string, or an object { url, filename?, contentType? } to control the attachment\'s displayed filename and MIME type.\n- When generateHtml is enabled, the body text is converted to a styled HTML email using an AI model (implies an HTML body).\n- Set replyTo to control the Reply-To address for replies.\n- For threaded replies in a shared inbox, set inReplyTo (the Message-ID being replied to) and references (prior Message-IDs in the thread).\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- Sends default to category "marketing": every message carries a one-click unsubscribe header pointing at the app\'s own domain, and recipients who previously unsubscribed from this app are skipped and returned in "suppressed". Because the unsubscribe link is per-recipient, a marketing send delivers one separate message per recipient and cc/bcc are folded into that list.\n- Set category to "transactional" for receipts, alerts, password resets and anything a recipient must not be able to opt out of. Transactional sends carry no unsubscribe header, ignore the unsubscribe list, and keep cc/bcc on a single message \u2014 use it whenever cc/bcc semantics matter.\n- Input: `{ subject: string, body: string, to?: string | string[], cc?: string | string[], bcc?: 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 | { url: string, filename?: string, contentType?: string })[], from?: string, bodyType?: "auto" | "html" | "markdown" | "text", text?: string, replyTo?: string, inReplyTo?: string, references?: string[], category?: "transactional" | "marketing" }`\n- Output: `{ recipients: string[], cc: string[], bcc: string[], from: string, suppressed: 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\nLegacy v1 data sources. v2 apps should use `dataSources.defineDataSource()` from @mindstudio-ai/agent instead, which owns parsing, chunking, embedding and citations. Upload 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### 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### 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### Meshy\n\n#### meshyAnimate\nApply a preset animation to a rigged 3D character model using Meshy.\n- Requires a rig_task_id from a previously completed Meshy rigging step.\n- Select an animation from Meshy\'s library of 600+ preset animations.\n- Only works with humanoid (bipedal) rigged characters.\n- Supports post-processing: FPS change (24/25/30/60), FBX-to-USDZ conversion, or armature extraction.\n- Animation categories: DailyActions, WalkAndRun, Fighting, Dancing, BodyMovements.\n- Input: `{ rigTaskId: string, actionId: number }`\n- Output: `{ glbUrl: string, fbxUrl?: string, objUrl?: string, usdzUrl?: string, thumbnailUrl?: string, textureUrls?: object[], animations?: { name: string, glbUrl?: string, fbxUrl?: string }[], providerTaskId?: string }`\n\n#### meshyImageTo3d\nGenerate a 3D model from one or more images using Meshy. Uses the multi-image-to-3D endpoint.\n- Accepts 1-4 image URLs. All images should depict the same object from different angles for best results.\n- By default generates with textures. Set shouldTexture to false for mesh-only output.\n- Uses should_remesh: false to preserve UV mapping integrity.\n- Input: `{ imageUrls: string[], shouldTexture?: boolean, topology?: string, targetPolycount?: number, symmetryMode?: string, poseMode?: string, textureImageUrl?: string }`\n- Output: `{ glbUrl: string, fbxUrl?: string, objUrl?: string, usdzUrl?: string, thumbnailUrl?: string, textureUrls?: object[], animations?: { name: string, glbUrl?: string, fbxUrl?: string }[], providerTaskId?: string }`\n\n#### meshyRemesh\nRemesh an existing 3D model to adjust topology, polygon count, or convert formats using Meshy.\n- Provide either an input task ID (from a previous Meshy step) or a model URL.\n- Defaults to triangle topology with 30,000 target polys.\n- Useful for reducing face count before rigging (max 300k faces for rigging).\n- Input: `{ inputTaskId?: string, modelUrl?: string, topology?: string, targetPolycount?: number, resizeHeight?: number }`\n- Output: `{ glbUrl: string, fbxUrl?: string, objUrl?: string, usdzUrl?: string, thumbnailUrl?: string, textureUrls?: object[], animations?: { name: string, glbUrl?: string, fbxUrl?: string }[], providerTaskId?: string }`\n\n#### meshyRig\nAuto-rig a humanoid 3D model and generate basic walking/running animations using Meshy.\n- Only works well with standard humanoid (bipedal) models with clearly defined limbs.\n- Prefers model_url over input_task_id for cleaner rigging input.\n- Models with more than 300,000 faces should be remeshed first.\n- Returns rigged model files and optional basic animations.\n- Input: `{ inputTaskId?: string, modelUrl?: string, heightMeters?: number }`\n- Output: `{ glbUrl: string, fbxUrl?: string, objUrl?: string, usdzUrl?: string, thumbnailUrl?: string, textureUrls?: object[], animations?: { name: string, glbUrl?: string, fbxUrl?: string }[], providerTaskId?: string }`\n\n#### meshyTextTo3d\nGenerate a 3D model preview from a text prompt using Meshy. Produces an untextured mesh (preview stage).\n- Creates a text-to-3D preview task (mesh generation only, no texture).\n- Use the Meshy Texture step to apply textures to the preview.\n- Maximum prompt length is 600 characters.\n- Input: `{ prompt: string, modelType?: string, topology?: string, targetPolycount?: number, symmetryMode?: string, poseMode?: string }`\n- Output: `{ glbUrl: string, fbxUrl?: string, objUrl?: string, usdzUrl?: string, thumbnailUrl?: string, textureUrls?: object[], animations?: { name: string, glbUrl?: string, fbxUrl?: string }[], providerTaskId?: string }`\n\n#### meshyTexture\nApply or replace textures on a 3D model using a text prompt or reference image via Meshy.\n- Provide either an input task ID (from a previous Meshy step) or a model URL.\n- Provide either a text style prompt or an image style URL to guide texturing.\n- Supports .glb, .gltf, .obj, .fbx, .stl model formats when using modelUrl.\n- By default preserves original UVs (enableOriginalUv = true).\n- Works with any model source: text-to-3D previews, image-to-3D, remeshed models, or external files.\n- Input: `{ inputTaskId?: string, modelUrl?: string, textStylePrompt?: string, imageStyleUrl?: string, enableOriginalUv?: boolean, enablePbr?: boolean }`\n- Output: `{ glbUrl: string, fbxUrl?: string, objUrl?: string, usdzUrl?: string, thumbnailUrl?: string, textureUrls?: object[], animations?: { name: string, glbUrl?: string, fbxUrl?: string }[], providerTaskId?: string }`\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### Particle Podcasts\n\n#### particlePodcastsFindMentions\nFind every dialogue line mentioning a specific entity or company across all podcasts.\n- Provide `entityId` (for people, products, places) OR `companyId` (for organizations). At least one is required.\n- Use `contextLines` to include surrounding dialogue with each mention (default behavior is set by Particle).\n- Resolve a name to an `entityId` / `companyId` first via Search Companies (or by inspecting a Search Dialogue response).\n- Cursor-paginated; expect potentially large result sets for popular entities.\n- Input: `{ entityId?: string, companyId?: string, contextLines?: number, limit?: number, cursor?: string }`\n- Output: `unknown`\n\n#### particlePodcastsGetEpisode\nFetch full metadata for a single episode: details, speakers, entities, clips, and ads \u2014 merged into one response.\n- Pass an episode ID or slug as `id`.\n- Returns five sub-resources merged: `episode` (metadata), `speakers` (diarized speaker list), `entities` (knowledge-graph mentions), `clips` (AI-extracted highlights), `ads` (detected ad spots).\n- Use Get Episode Transcript separately when you need the full transcript text \u2014 it isn\'t bundled here because the payload is large and has its own format/range options.\n- Bills as 5 units against the get-episode event type (one per sub-call).\n- Input: `{ id: string }`\n- Output: `unknown`\n\n#### particlePodcastsGetEpisodeTranscript\nFetch the diarized transcript for an episode in dialogue, plain text, or SRT subtitle format.\n- Pass an episode ID or slug as `id`.\n- Use `format` = "dialogue" (default, with speaker turns), "text" (plain), or "srt" (subtitle).\n- Filter to a single speaker with `speaker`, or to a time range with `start` / `end` (seconds).\n- Transcripts are large \u2014 prefer time-range filtering when you only need a snippet.\n- Input: `{ id: string, format?: "dialogue" | "text" | "srt", speaker?: string, start?: number, end?: number }`\n- Output: `unknown`\n\n#### particlePodcastsSearchCompanies\nSearch the Particle knowledge graph for companies by name, ticker, domain, CIK, or QID.\n- Provide one or more identifiers: `q` (free-text name), `ticker` (e.g. "TSLA"), `domain` (e.g. "tesla.com"), `cik` (SEC), or `qid` (Wikidata).\n- Use this to resolve a company name to a canonical `companyId` for use with Find Mentions or Search Dialogue.\n- Returned company objects include slugs, domains, and IDs \u2014 any of these can be passed to downstream blocks.\n- Input: `{ q?: string, ticker?: string, domain?: string, cik?: string, qid?: string, entityId?: string, updatedAfter?: string, limit?: number, cursor?: string }`\n- Output: `unknown`\n\n#### particlePodcastsSearchDialogue\nSearch across podcast dialogue using semantic or keyword search. Returns matched lines grouped by episode.\n- Provide `semanticSearch` for meaning-based discovery ("find moments where someone talks about market timing") or `keywordSearch` for exact phrase/proper-noun matching. At least one must be provided.\n- Filter to a specific entity or company by passing `entityId` / `companyId`.\n- Each returned dialogue line carries the source episode + speaker so you can chain to Get Episode or Get Episode Transcript for context.\n- Cursor-paginated.\n- Input: `{ semanticSearch?: string, keywordSearch?: string, entityId?: string, companyId?: string, limit?: number, cursor?: string }`\n- Output: `unknown`\n\n#### particlePodcastsSearchPodcasts\nSearch and list podcasts in the Particle catalog by keyword, topic, or language.\n- Use `q` for free-text keyword search across podcast titles and descriptions.\n- Use `topic` to filter to a Particle taxonomy topic.\n- Use `language` (BCP 47, e.g. "en") to restrict to a language.\n- Returns podcast objects with canonical IDs and slugs. Pass either to other Particle Podcasts blocks.\n- Cursor-paginated; pass the returned `cursor` back to `cursor` for the next page.\n- Input: `{ q?: string, topic?: string, language?: string, suitabilityTier?: string, limit?: number, cursor?: string }`\n- Output: `unknown`\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### You.com\n\n#### youDotComFinanceResearch\nAsk a financial research question using You.com Finance Research and return the sourced response.\n- Use this for financial questions such as company analysis, earnings, market research, filings, macroeconomics, and due diligence.\n- researchEffort supports deep (default) or exhaustive.\n- Finance Research returns the same response shape as Web Research, but searches a finance-optimized index.\n- Use it for cited synthesis, not raw price feeds or structured time-series exports.\n- Input: `{ input: string, researchEffort?: "deep" | "exhaustive" }`\n- Output: `{ data: object | unknown[] }`\n\n#### youDotComGetPageContent\nFetch clean Markdown, HTML, or metadata for known URLs using the You.com Contents API.\n- Use this step when you already know the URLs. Use Web Search with livecrawl when You.com should discover pages from a query.\n- A single request supports up to 10 URLs.\n- Request only the formats you need. Markdown is recommended for LLM consumption.\n- Increase crawlTimeout for JavaScript-heavy pages, up to 60 seconds.\n- Individual pages can partially fail; check each returned item before processing.\n- Input: `{ urls: string[], formats?: ("markdown" | "html" | "metadata")[], crawlTimeout?: number }`\n- Output: `{ data: object | unknown[] }`\n\n#### youDotComLiveNews\nFetch live news articles through the You.com Search API and return the full structured response.\n- Defaults freshness to day for breaking or recent news.\n- Use country and language together to monitor regional or non-English news.\n- Use livecrawl: \'news\' with livecrawlFormats: [\'markdown\'] when you need full article text.\n- Use a custom freshness range like YYYY-MM-DDtoYYYY-MM-DD for historical news windows.\n- Input: `{ query: string, freshness?: string, count?: number, country?: string, language?: string, safesearch?: "off" | "moderate" | "strict", livecrawl?: "news" | "all", livecrawlFormats?: ("markdown" | "html")[] }`\n- Output: `{ data: object | unknown[] }`\n\n#### youDotComWebResearch\nAsk a research question and return a grounded You.com Research API answer with sources.\n- Use Web Search when you need raw URLs and snippets. Use Web Research when you want a synthesized answer with citations.\n- researchEffort controls depth and latency: lite, standard, deep, or exhaustive. standard is a good default.\n- sourceControl can restrict, exclude, or boost domains, and can apply freshness or country filters.\n- includeDomains cannot be combined with excludeDomains or boostDomains.\n- outputSchema returns structured output.content and is supported by standard, deep, and exhaustive, not lite.\n- Input: `{ input: string, researchEffort?: "lite" | "standard" | "deep" | "exhaustive", sourceControl?: { includeDomains?: string[], excludeDomains?: string[], boostDomains?: string[], freshness?: string, country?: string } | string, outputSchema?: object | string }`\n- Output: `{ data: object | unknown[] }`\n\n#### youDotComWebSearch\nSearch the web and news using the You.com Search API and return the full structured response.\n- Query supports You.com search operators:\n| Operator | Description | Example |\n| -------- | ----------- | ------- |\n| site: | Search within a domain and its subdomains | site:uscourts.gov |\n| filetype: | Search for a specific file type | filetype:pdf |\n| + | Require the exact term after the operator | +GAAP |\n| - | Exclude the exact term after the operator | -prs |\n| AND | Require both expressions | guitar AND Fender |\n| OR | Match either expression | guitar OR drum |\n| NOT | Negate an expression | NOT site:uscourts.gov |\n- Use livecrawl with livecrawlFormats: [\'markdown\'] when you need full page content instead of snippets.\n- Use the Get Page Content step when you already know the URLs to fetch.\n- Use freshness for recency: day, week, month, year, or YYYY-MM-DDtoYYYY-MM-DD.\n- Use country (ISO 3166-1 alpha-2) and language (BCP 47) to focus results geographically or linguistically.\n- includeDomains cannot be combined with excludeDomains or boostDomains.\n- Input: `{ query: string, count?: number, freshness?: string, country?: string, language?: string, offset?: number, safesearch?: "off" | "moderate" | "strict", livecrawl?: "web" | "news" | "all", livecrawlFormats?: ("markdown" | "html")[], crawlTimeout?: number, includeDomains?: string[], excludeDomains?: string[], boostDomains?: string[] }`\n- Output: `{ data: object | unknown[] }`\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 nanodollars (1e-9 USD)\n unitType: string; // What constitutes a unit (e.g. "token", "request")\n estimatedCost?: number; // Estimated total cost in nanodollars, 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)`\n**Deprecated for app file storage** \u2014 use the `files` store below (`files.defineStore(...).put(...)`): private by default, app-scoped, and served on the app\'s own domain. `uploadFile` uploads to the shared account media CDN and remains only for account-level assets (e.g. an agent avatar for `changeProfilePicture`). 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\n#### `files` \u2014 per-app file storage (private by default)\nTyped per-app file storage: user uploads, generated documents, images, marketing assets. Think of a store as a CDN-backed bucket the app talks to \u2014 NOT app-defined state like a `db` table. The API is *shaped* like the `db` namespace (define a store at module scope, then read/write), but its contents aren\'t modeled by code, and one store is shared across dev and prod (a file uploaded in the dev editor is already live in prod at the same URL). Creates are safe by default because keys default to unique (UUID, or a content-addressed hash); the operations to treat with care are `delete(key)` and overwriting a fixed key. Public objects are CDN-cached per their `Cache-Control`, set at put time via `cacheControl`: auto-minted keys default to immutable (cache-forever \u2014 the key is never reused), and named keys default to `public, max-age=300`, so overwriting one propagates publicly within ~5 minutes; pass `cacheControl` to tune (e.g. `\'public, max-age=60\'` for near-live config, `\'no-store\'` to revalidate every read). Files are served on the app\'s own domain: private stores (the default) are signed / session-authorized; public stores are world-readable, CDN-served, and images resize via query params (e.g. `?w=400&h=300&fit=cover`). Prefer this over the deprecated `uploadFile()` for app storage.\n\n```typescript\nimport { files } from \'@mindstudio-ai/agent\';\n\n// Define stores at module scope (access is pinned here).\nexport const Uploads = files.defineStore(\'uploads\'); // private (default)\nexport const Assets = files.defineStore(\'assets\', { access: \'public\' });\n\n// Store bytes and get a ready-to-use URL.\nconst file = await Uploads.put(buffer, { contentType: \'application/pdf\', filename: \'report.pdf\' });\nfile.url; // stable on-domain URL for a logged-in user (drop into <img>/<a download>)\nawait file.shareUrl(); // absolute signed URL that works with NO session (email/embed); default 24h\n```\n\nStore methods:\n- `put(content, { key?, contentType?, filename?, contentAddressed?, cacheControl? })` \u2192 `StoredFile`. `content` is `Buffer | Uint8Array | string`. Omit `key` for a random UUID; `contentAddressed: true` derives an immutable `<sha256>.<ext>` key. `cacheControl` sets the CDN caching of a public object (defaults: auto-minted keys immutable, named keys `public, max-age=300`).\n- `get(key)` \u2192 `Buffer` \xB7 `head(key)` \u2192 `StoredFile` \xB7 `exists(key)` \u2192 `boolean`\n- `list({ prefix?, cursor?, limit? })` \u2192 `{ files: StoredFile[]; cursor? }`\n- `delete(key)` \u2192 `void`\n- `shareUrl(key, { expiresIn? })` \u2192 signed absolute URL (private stores; no session needed)\n\n`StoredFile`: `{ store, key, access, size?, contentType?, updatedAt?, url, shareUrl() }`.\n\n`defineStore(name, options?)` options: `access: \'public\' | \'private\'` (default private), `maxSize` (bytes; cap for client-direct uploads), `contentTypes` (allowlist for client-direct uploads).\n\nUser uploads (client-direct \u2014 bytes go straight to storage, never through the app backend): a backend method mints a token, the frontend submits it with `@mindstudio-ai/interface`.\n```typescript\n// backend method\nconst token = await Uploads.createUploadToken({ contentType, maxSize: 25 * 1024 * 1024 }); // also accepts cacheControl\nreturn token;\n// frontend (@mindstudio-ai/interface)\nimport { platform } from \'@mindstudio-ai/interface\';\nconst { url } = await platform.upload(token, file, { onProgress });\n```\n\nGenerated assets: any action that produces a file (`generateImage`, `generateVideo`, `generateSpeech`, `generatePdf`, `upscaleImage`, \u2026) can put its output straight into one of these stores \u2014 pass the store handle as `store` in the OPTIONS object (second argument). Optional: omit it and the asset goes to the shared MindStudio CDN as before. Reach for it when the asset belongs to the app \u2014 private to a user, or listed alongside its other files.\n\n```typescript\nconst { imageUrl } = await mindstudio.generateImage({ prompt: \'a red sneaker\' }, { store: Assets });\n```\n\n#### `db` \u2014 typed tables over the app\'s managed SQLite\n\nChainable, typed collection API over the app\'s managed databases. Define a table at module scope; every read/write is lazy until awaited, and reads/writes can be batched into one round trip with `db.batch()`.\n```typescript\nimport { db } from \'@mindstudio-ai/agent\';\n\nexport const Orders = db.defineTable<Order>(\'orders\');\n\n// Reads \u2014 filters compile to SQL WHERE when possible\nconst recent = await Orders.filter(o => o.status === \'paid\').sortBy(o => o.created_at).reverse().take(20);\n// Comparing to an outer-scope value? Use the bindings form so it compiles to SQL:\nconst mine = await Orders.filter((o, $) => o.requestedBy === $.uid, { uid: auth.userId }); // bindings: lifts closure var so filter compiles to SQL\n\n// Writes\nconst order = await Orders.push({ item: \'Laptop\', amount: 999, status: \'pending\' });\nawait Orders.update(order.id, { status: \'approved\' });\n```\n\nAggregation \u2014 `count()`, `sum(fn)`, `avg(fn)`, `countDistinct(fn)`, and `aggregate(spec)` compile to SQL aggregates (COUNT/TOTAL/AVG/GROUP BY); no rows are fetched, so they stay cheap at any table size. Never page a whole table into memory to compute a summary.\n```typescript\nconst top = await Answers\n .filter((a, $) => a.surveyId === $.surveyId, { surveyId }) // bindings: compiles to SQL\n .aggregate({\n by: [\'questionId\', \'dimension\'],\n select: { n: { count: true }, total: { sum: \'score\' }, avgScore: { avg: \'score\' }, respondents: { countDistinct: \'responseId\' } },\n orderBy: \'total\', desc: true, limit: 20,\n });\n// Array<{ questionId, dimension, n: number, total: number, avgScore: number | null, respondents: number }>\n```\nSelect terms: `{ count: true }` \xB7 `{ sum: \'col\' }` \xB7 `{ avg: \'col\' }` \xB7 `{ min: \'col\' }` \xB7 `{ max: \'col\' }` \xB7 `{ countDistinct: \'col\' }` (string column names). Omit `by` for whole-set aggregates (single object). Empty set: count/countDistinct 0, sum 0, avg/min/max null.\n\nRaw SQL escape hatch \u2014 `db.sql<T>(query, params?, { database? })` runs read-only raw SQL (joins, subqueries, window functions) against the app\'s own managed database. SELECT/WITH only; writes throw \u2014 use Table methods. Lazy and batchable. Last resort: prefer the typed API (including `aggregate()`) whenever it can express the query; raw rows may not exactly match the typed API\'s value representations.\n```typescript\nconst rows = await db.sql<{ questionId: string; n: number }>(\'SELECT questionId, COUNT(*) AS n FROM answers WHERE surveyId = ? GROUP BY questionId\', [surveyId]);\n```\n\n#### `dataSources` \u2014 searchable document corpora (RAG)\n\nDeclare a data source at module scope and import the handle. The platform owns parsing, chunking, embedding, storage and tenant isolation; every hit returns a citation.\n```typescript\nimport { dataSources } from \'@mindstudio-ai/agent\';\nexport const Policies = dataSources.defineDataSource(\'policies\');\n\n// search\nconst { results } = await Policies.search(\'what are the payment terms?\', { topK: 5 });\n// options: topK (default 5, max 50), scoreThreshold, filter, mode, maxPerDocument, highlight, rerank, hybrid\nconst context = results.map((r) => r.text).join(String.fromCharCode(10, 10));\n\n// narrow before ranking: metadata tags, filename, pages, required words\nawait Policies.search(\'termination clause\', {\n filter: { metadata: { department: \'legal\', year: [2025, 2026] }, phrase: \'notice period\' },\n maxPerDocument: 2, // stop one document monopolizing the results\n});\n\n// mode: \'hybrid\' (default) | \'semantic\' | \'lexical\'. Lexical skips the query\n// embedding \u2014 fastest, right for identifier-shaped queries (error codes, SKUs).\nawait Policies.search(\'ERR-7741X\', { mode: \'lexical\' });\n\n// add a document (background \u2014 poll documents() for status)\nawait Policies.add(buffer, { filename: \'policy.pdf\', contentType: \'application/pdf\', metadata: { department: \'legal\' } });\nconst docs = await Policies.documents();\n\n// introspection \u2014 what is in the corpus, and how one document was split\nconst { chunkCount, pipeline } = await Policies.stats(); // counts + the config actually in effect\nconst chunks = await Policies.chunks(docs[0].id); // why a document does or doesn\'t match\n```\n\n`SearchHit`: `{ score, text, citation: { documentId, filename, pageNumber, chunkIndex, headingPath, boundingBox?, url } }`. `citation.url` is a stable on-domain link to the source document \u2014 drop it in an `<a href>`. `(documentId, chunkIndex)` is a chunk\'s stable identity \u2014 key an eval set on that, not on `text`.\n\nAlso on each hit: `retrievalRank`/`retrievalScore` (position before reranking \u2014 compare with the final position to see what reranking did). Three opt-in search options for debugging and display: `explain: true` adds `explain.{dense,lexical,matchedVia}` so you can see which half of hybrid found a hit; `expand: 1` adds `neighbors.{before,after}` for surrounding context; `highlight: true` adds `matches` \u2014 `{start, end}` offsets of query terms in `text`, for rendering highlights. None of them changes the results or their order.\n\n`filter` fields (all AND together, and only narrow): `metadata` (per-key equals, or any-of via an array), `filename`, `documentIds`, `pages: {min?, max?}`, `contains` (all words, any order), `phrase` (exact adjacent sequence). Document metadata is set at add time: scalars only, \u226416 keys, keys `[a-zA-Z0-9_-]`. Re-adding the same bytes with different metadata updates the tags in place \u2014 no re-processing.\n\nSearch is deterministic for a fixed corpus and configuration \u2014 same query, same order \u2014 so regression checks are meaningful. `search()` also returns `latencyMs`.\n\nOther methods: `stats()`, `documents()`, `chunks(documentId, {vectors?})`, `remove(documentId)`, `ensure(name?)` (rarely needed \u2014 `add` and `search` create-on-reference), and `DataSource.contentHash(bytes)` to check for an existing document before adding.\n\nSupported: pdf, docx, pptx, xlsx, odt, rtf, epub, images, txt, md, json, csv, tsv, log, html.\n\nAdding the same bytes twice is free \u2014 documents are content-addressed, so a re-add is a no-op when this source has already processed those exact bytes under its current configuration. Only reconfiguring the source makes a re-add do work again, and that is an explicit owner-triggered migration, never a side effect of a deploy.\n\nEverything in `SearchOptions` \u2014 `mode`, `filter`, `rerank`, `hybrid`, `maxPerDocument`, `highlight` \u2014 is free to change and takes effect on the next search. How documents are chunked and embedded is configured on the corpus itself with `mindstudio-prod datasources config` \u2014 not in code \u2014 and changing it on a populated source is rejected rather than silently rebuilding.\n\n**A data source is live and shared.** Unlike database tables there is no dev copy and no per-release isolation: adding or removing a document changes what the deployed app retrieves, immediately. Scenarios never reset a data source, and re-ingesting a large corpus costs real money.\n\n#### `analytics` \u2014 read the app\'s own traffic + event analytics\n\nThe platform auto-tracks pageviews (and custom events via the frontend\'s `analytics.track()`); the backend `analytics` namespace is the read side, so app code can correlate real traffic with its own data \u2014 lifetime views next to each blog post, "N people reading now", conversions beside the records that produced them. Backend only (hook token); reads the LIVE app\'s real traffic even from a dev sandbox. Rate limited per app (120 reads/min).\n\nOne general read, `query()`, shaped like the Plausible/GA4 query APIs:\n\n```typescript\nimport { analytics } from \'@mindstudio-ai/agent\';\n\n// Lifetime pageviews + unique visitors per page \u2014 top 10.\nconst top = await analytics.query({\n metrics: [\'pageviews\', \'visitors\'],\n dimensions: [\'path\'],\n dateRange: \'all\',\n limit: 10,\n});\n// top.results[0] \u2192 { dimensions: { path: \'/post/hello\' }, metrics: { pageviews: 4210, visitors: 1830 } }\n\n// Daily views for one post, full history.\nconst series = await analytics.query({\n metrics: [\'pageviews\'],\n dimensions: [\'time\'],\n granularity: \'day\',\n filters: [[\'is\', \'path\', [\'/post/hello\']]],\n dateRange: \'all\',\n});\n```\n\nGrammar: `metrics` from `pageviews | visitors | visits | events`; `dimensions` is at most ONE entity dimension (`path`, `referrerHost`, `sourceCategory`, `country`, `city`, `deviceType`, `browser`, `os`, `language`, `visitorType`, `utmSource`\u2026`utmContent`, `eventName`) OR `\'time\'` (with `granularity: \'5m\'|\'hour\'|\'day\'|\'week\'|\'month\'` and optional IANA `timezone`); `filters` are `[op, dimension, values]` triples with ops `is | is_not | contains`; `dateRange` is a shorthand (`\'1h\'|\'24h\'|\'7d\'|\'30d\'|\'90d\'|\'all\'`) or `[startISO, endISO]`; `orderBy` (grouped only) accepts `pageviews` or `events`. With `dateRange: \'all\'`, granularity is a minimum \u2014 the server coarsens as history grows. `city` filter values are plain city names (any country) and always read the 90-day event detail.\n\n**Retention split \u2014 the one thing to get right:** queries whose filters are all `is` and touch at most one dimension read a rollup kept forever (full lifetime history, exact unique visitors). Cross-dimension intersections, `is_not`, and `contains` scan raw events retained for 90 days; the server clamps the window and reports it. Check `meta`: `source` (`rollup` vs `events`), `clamped`, `window.served`, and `metricsOmitted` \u2014 metrics a shape can\'t carry (e.g. `visits` on grouped reads) are omitted there, never returned as silent zeros. `meta.total` carries the unpaginated group count for paging.\n\nSpecials (same `dateRange`/filter-triple grammar): `analytics.live()` \u2192 `{count, countries, sparkline}` right now; `analytics.sources(opts?)` \u2192 per-session first-source ranking (UTM > referrer > direct) classified by category/vendor incl. AI assistants; `analytics.map(opts?)` \u2192 city lat/lon points; `analytics.aiSources(opts?)` \u2192 AI-assistant referrals per vendor; `analytics.crawlers.overview()/timeseries()/recent()` \u2192 bot/AI-crawler ingestion of the app\'s pages.\n';
7442
8059
  }
7443
8060
  });
7444
8061
 
@@ -7695,7 +8312,7 @@ async function startMcpServer(options) {
7695
8312
  capabilities: { tools: {} },
7696
8313
  serverInfo: {
7697
8314
  name: "mindstudio-agent",
7698
- version: "0.1.103"
8315
+ version: "0.1.105"
7699
8316
  },
7700
8317
  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, pass its public image URL to `changeProfilePicture`.\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.'
7701
8318
  });
@@ -8583,7 +9200,7 @@ function isNewerVersion(current, latest) {
8583
9200
  return false;
8584
9201
  }
8585
9202
  async function checkForUpdate() {
8586
- const currentVersion = "0.1.103";
9203
+ const currentVersion = "0.1.105";
8587
9204
  if (!currentVersion) return null;
8588
9205
  try {
8589
9206
  const { loadConfig: loadConfig2, saveConfig: saveConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
@@ -8612,7 +9229,7 @@ async function checkForUpdate() {
8612
9229
  }
8613
9230
  }
8614
9231
  function printUpdateNotice(latestVersion) {
8615
- const currentVersion = "0.1.103";
9232
+ const currentVersion = "0.1.105";
8616
9233
  process.stderr.write(
8617
9234
  `
8618
9235
  ${ansi2.cyanBright("Update available")} ${ansi2.gray(currentVersion + " \u2192")} ${ansi2.cyanBold(latestVersion)}
@@ -8625,7 +9242,7 @@ function isStandaloneBinary() {
8625
9242
  return !argv1.includes("node_modules");
8626
9243
  }
8627
9244
  async function cmdUpdate() {
8628
- const currentVersion = "0.1.103";
9245
+ const currentVersion = "0.1.105";
8629
9246
  process.stderr.write(
8630
9247
  ` ${ansi2.gray("Current version:")} ${currentVersion}
8631
9248
  `
@@ -8740,7 +9357,7 @@ async function cmdLogin(options) {
8740
9357
  process.stderr.write("\n");
8741
9358
  printLogo();
8742
9359
  process.stderr.write("\n");
8743
- const ver = "0.1.103";
9360
+ const ver = "0.1.105";
8744
9361
  process.stderr.write(
8745
9362
  ` ${ansi2.bold("MindStudio Agent")} ${ver ? " " + ansi2.gray("v" + ver) : ""}
8746
9363
  `
@@ -9051,7 +9668,7 @@ async function main() {
9051
9668
  try {
9052
9669
  if (command === "version" || command === "-v") {
9053
9670
  process.stdout.write(
9054
- "0.1.103\n"
9671
+ "0.1.105\n"
9055
9672
  );
9056
9673
  return;
9057
9674
  }