taikutaiku
User GuidePluginsAPI Reference

External Integrations

Connect a plugin to GitHub, Linear, Slack, Gmail, and other SaaS apps, then list and run their tools.

Plugins can reach external SaaS apps -- GitHub, Linear, Slack, Gmail, and hundreds of others -- through taiku's integrations API. You declare the toolkits your plugin needs in its manifest, then call four bridge methods to connect a user's account over OAuth, discover the toolkit's tools, and run them. taiku handles the OAuth handshake and token storage for you; your plugin never sees a secret or an access token.

A full working example ships in the default registry as the Integrations Demo plugin (example.integrations). It connects GitHub, lists its tools, and runs one action -- the snippets below are condensed from it.

How it works

Each integrations call goes from your sandboxed iframe to the taiku host over the usual postMessage bridge. The host checks that your manifest declares the permission and the toolkit, then proxies the request to the server, which talks to the integrations provider as the signed-in taiku user. Connections are keyed to the user's account, so once someone connects GitHub their tokens persist across sessions.

Two consequences matter when you build:

  • Integrations require a signed-in account. Anonymous sessions cannot own credentials, so calls fail there.
  • A plugin can only touch toolkits it declared. A call to a toolkit missing from manifest.integrations is rejected by the host before any network request -- declaring ["github"] does not let you reach slack.

Manifest setup

Declare the toolkit slugs your plugin uses and request the integrations permissions you need. Slugs are integration toolkit names and are normalized to lowercase, so "GitHub" and "github" are the same toolkit.

{
  "id": "my-org.issue-helper",
  "name": "Issue Helper",
  "version": "1.0.0",
  "permissions": [
    "integrations.read",
    "integrations.connect",
    "integrations.execute",
    "ui.panel",
    "ui.toolbar.custom"
  ],
  "integrations": ["github", "linear"],
  "panels": [
    {
      "id": "issues-panel",
      "title": "Issue Helper",
      "position": "sidebar",
      "htmlUrl": "panel.html",
      "defaultWidth": 360
    }
  ]
}

Permissions

PermissionLets your plugin
integrations.readList the user's connections and browse a toolkit's tools.
integrations.connectStart an OAuth connection to a declared toolkit.
integrations.executeRun a tool on a declared toolkit as the signed-in user.

integrations.connect and integrations.execute are meaningless without a toolkit list, so the manifest must include a non-empty integrations array when you request either one -- taiku rejects the plugin at load time otherwise. integrations.read on its own (for example, to show connection status) does not require the list, but you still cannot browse a toolkit's tools unless it is declared.

Each slug must match ^[a-z0-9_-]+$. Declaring the toolkits up front means the user sees exactly which external apps your plugin can reach when they install it.

The bridge call pattern

Integrations methods are invoked over the raw bridge protocol. Send a taiku:request message to the host and resolve the matching taiku:response. This sendAsync helper -- the same one the Integrations Demo uses -- captures the pattern:

let pluginId = "",
  panelId = "",
  authToken = "",
  reqId = 0;
const hostWindow = window.parent;

function sendAsync(method, params) {
  return new Promise((resolve, reject) => {
    const id = "ig_" + ++reqId;
    hostWindow.postMessage(
      {
        type: "taiku:request",
        id,
        pluginId,
        panelId,
        authToken,
        method,
        params,
      },
      "*",
    );
    const handler = (e) => {
      if (e.source !== hostWindow) return;
      const d = e.data;
      if (d?.type === "taiku:response" && d.id === id) {
        window.removeEventListener("message", handler);
        if (d.error) reject(new Error(d.error));
        else resolve(d.result);
      }
    };
    window.addEventListener("message", handler);
    setTimeout(() => {
      window.removeEventListener("message", handler);
      reject(new Error("Timeout"));
    }, 20000);
  });
}

// Capture the identity the host sends on init before making any calls.
window.addEventListener("message", (e) => {
  if (e.source !== hostWindow) return;
  const d = e.data;
  if (d?.type === "taiku:init") {
    pluginId = d.pluginId;
    panelId = d.panelId || "";
    authToken = d.authToken || "";
  }
});

A failed call rejects the promise with the error text from the server or the integration, so wrap calls in try/catch.

Methods

integrations.list

Lists the signed-in user's connected accounts. No params. Requires integrations.read.

const { connections } = await sendAsync("integrations.list");
// connections: [{ id, toolkit, status }]
const gh = connections.find((c) => c.toolkit === "github");
const connected = gh && /active/i.test(gh.status || "");

A connection shows up here as soon as it is created, with status advancing to ACTIVE once the user finishes OAuth.

integrations.connect

Starts an OAuth connection to a toolkit. Requires integrations.connect, and the toolkit must be declared in your manifest.

const res = await sendAsync("integrations.connect", { toolkit: "github" });
// res: { connectionId, redirectUrl, status, opened }
ParamTypeNotes
toolkitstringRequired. Must be a declared toolkit.
callbackUrlstring?Where the browser lands after consent. Defaults to the app origin.

The host opens redirectUrl in a popup so the user can complete the OAuth flow. Because plugin iframes are sandboxed, your code does not open the popup -- the host does, and it reports whether the popup actually opened:

  • opened: true -- the popup opened. Tell the user to finish OAuth in the new tab, then re-check status with integrations.list.
  • opened: false -- a popup blocker stopped it. Render redirectUrl as a clickable link so the user can open it manually.
async function connect() {
  const res = await sendAsync("integrations.connect", { toolkit: "github" });
  if (!res?.redirectUrl) return;
  if (res.opened) {
    hint.textContent = "Finish connecting in the opened tab, then refresh.";
  } else {
    hint.innerHTML =
      'Popup blocked — <a target="_blank" rel="noopener" href="' +
      res.redirectUrl +
      '">click here to connect</a>, then refresh.';
  }
}

After the user completes consent, a follow-up integrations.list shows the connection as ACTIVE.

integrations.tools

Lists a toolkit's available actions and their input schemas. Requires integrations.read, and the toolkit must be declared.

const { tools } = await sendAsync("integrations.tools", {
  toolkit: "github",
  search: "issue",
  limit: 25,
});
// tools: [{ slug, name, description, inputParameters }]
ParamTypeNotes
toolkitstringRequired. Must be a declared toolkit.
searchstring?Free-text filter over tool names.
limitnumber?Max tools to return (clamped to 100; defaults to 50).

Each tool's slug is the action name you pass to integrations.execute (for example GITHUB_CREATE_AN_ISSUE), and inputParameters is its JSON-schema-style argument shape.

integrations.execute

Runs a tool as the signed-in user. Requires integrations.execute, and the toolkit must be declared.

const result = await sendAsync("integrations.execute", {
  toolkit: "github",
  action: "GITHUB_CREATE_AN_ISSUE",
  arguments: {
    owner: "my-org",
    repo: "my-repo",
    title: "Bug: build fails on main",
    body: "Steps to reproduce…",
  },
});
ParamTypeNotes
toolkitstringRequired. Must be a declared toolkit.
actionstringRequired. A tool slug from integrations.tools.
argumentsobjectArguments for the tool. Defaults to {}.

The return value is the integration's raw tool result, forwarded verbatim -- including the exact error body when the tool itself fails -- so you see precisely what the upstream API returned.

Worked example

A minimal panel that connects GitHub, lists its tools, and runs the selected one. It assumes the sendAsync helper and taiku:init handler from The bridge call pattern are already in scope.

<button id="connect">Connect GitHub</button>
<button id="load">List tools</button>
<select id="tools"></select>
<textarea id="args">{}</textarea>
<button id="run">Run</button>
<pre id="output"></pre>

<script>
  const TOOLKIT = "github";
  const $ = (id) => document.getElementById(id);

  async function refresh() {
    const { connections } = await sendAsync("integrations.list");
    const conn = connections.find((c) => c.toolkit === TOOLKIT);
    $("connect").textContent =
      conn && /active/i.test(conn.status || "")
        ? "Reconnect GitHub"
        : "Connect GitHub";
  }

  $("connect").onclick = async () => {
    try {
      const res = await sendAsync("integrations.connect", { toolkit: TOOLKIT });
      if (res.opened) {
        $("output").textContent =
          "Finish OAuth in the opened tab, then reload.";
      } else if (res.redirectUrl) {
        $("output").innerHTML =
          'Popup blocked: <a target="_blank" rel="noopener" href="' +
          res.redirectUrl +
          '">connect here</a>.';
      }
    } catch (err) {
      $("output").textContent = "Connect failed: " + err.message;
    }
  };

  $("load").onclick = async () => {
    try {
      const { tools } = await sendAsync("integrations.tools", {
        toolkit: TOOLKIT,
        limit: 50,
      });
      $("tools").innerHTML = "";
      for (const t of tools) {
        const opt = document.createElement("option");
        opt.value = t.slug;
        opt.textContent = t.slug;
        opt.title = t.description || "";
        $("tools").appendChild(opt);
      }
    } catch (err) {
      $("output").textContent = "List failed: " + err.message;
    }
  };

  $("run").onclick = async () => {
    let args;
    try {
      args = JSON.parse($("args").value || "{}");
    } catch {
      $("output").textContent = "Arguments must be valid JSON.";
      return;
    }
    try {
      const result = await sendAsync("integrations.execute", {
        toolkit: TOOLKIT,
        action: $("tools").value,
        arguments: args,
      });
      $("output").textContent = JSON.stringify(result, null, 2);
    } catch (err) {
      $("output").textContent = "Error: " + err.message;
    }
  };

  refresh();
</script>

Error handling

Every method rejects its promise with the failure text when something goes wrong, so a single try/catch per call is enough:

try {
  await sendAsync("integrations.execute", {
    toolkit: "github",
    action: "GITHUB_CREATE_AN_ISSUE",
    arguments: { owner, repo, title },
  });
} catch (err) {
  // err.message is the server or integration error text.
  showToast("Could not create issue: " + err.message, "error");
}

Common failure causes:

  • Undeclared toolkit -- the toolkit is missing from manifest.integrations. The host rejects this locally before any request is made.
  • Missing permission -- the method requires a permission your manifest did not declare.
  • Not signed in -- integrations require a signed-in account; anonymous sessions are rejected.
  • The tool itself failed -- integrations.execute forwards the integration's error body verbatim, so inspect err.message (or the returned result) for the upstream detail.
  • Integrations not enabled -- if your taiku server has not turned on integrations, every call fails. Ask your taiku admin to enable integrations.

Where to go next

  • For the full bridge and permission surface, see the Plugin API.
  • For the manifest, panels, and SDK basics, see Building Plugins.
  • To run the reference implementation, enable the Integrations Demo plugin from the plugin panel in any session.

On this page