diff --git a/README.md b/README.md
index fe4dccd0..13b4b23f 100644
--- a/README.md
+++ b/README.md
@@ -86,12 +86,14 @@ Visit [http://localhost:3000](http://localhost:3000) to get started.
- **Firecrawl**: Scrape URL, Search Web
- **GitHub**: Create Issue, List Issues, Get Issue, Update Issue
- **Linear**: Create Ticket, Find Issues
+- **Mixedbread**: Ingest File
- **Perplexity**: Search Web, Ask Question, Research Topic
- **Resend**: Send Email
- **Slack**: Send Slack Message
- **Stripe**: Create Customer, Get Customer, Create Invoice
- **Superagent**: Guard, Redact
- **v0**: Create Chat, Send Message
+- **Webflow**: List Sites, Get Site, Publish Site
## Code Generation
diff --git a/plugins/index.ts b/plugins/index.ts
index 495c6e33..5d58ba06 100644
--- a/plugins/index.ts
+++ b/plugins/index.ts
@@ -21,6 +21,7 @@ import "./fal";
import "./firecrawl";
import "./github";
import "./linear";
+import "./mixedbread";
import "./perplexity";
import "./resend";
import "./slack";
diff --git a/plugins/mixedbread/credentials.ts b/plugins/mixedbread/credentials.ts
new file mode 100644
index 00000000..9bf97633
--- /dev/null
+++ b/plugins/mixedbread/credentials.ts
@@ -0,0 +1,3 @@
+export type MixedbreadCredentials = {
+ MIXEDBREAD_API_KEY?: string;
+};
diff --git a/plugins/mixedbread/icon.tsx b/plugins/mixedbread/icon.tsx
new file mode 100644
index 00000000..4b4f0be7
--- /dev/null
+++ b/plugins/mixedbread/icon.tsx
@@ -0,0 +1,77 @@
+export function MixedbreadIcon({ className }: { className?: string }) {
+ return (
+
+ );
+}
diff --git a/plugins/mixedbread/index.ts b/plugins/mixedbread/index.ts
new file mode 100644
index 00000000..c7ad89fe
--- /dev/null
+++ b/plugins/mixedbread/index.ts
@@ -0,0 +1,96 @@
+import type { IntegrationPlugin } from "../registry";
+import { registerIntegration } from "../registry";
+import { MixedbreadIcon } from "./icon";
+
+const mixedbreadPlugin: IntegrationPlugin = {
+ type: "mixedbread",
+ label: "Mixedbread",
+ description: "Upload files to Mixedbread document stores",
+
+ icon: MixedbreadIcon,
+
+ formFields: [
+ {
+ id: "mixedbreadApiKey",
+ label: "API Key",
+ type: "password",
+ placeholder: "mxbai-...",
+ configKey: "mixedbreadApiKey",
+ envVar: "MIXEDBREAD_API_KEY",
+ helpText: "Get your API key from ",
+ helpLink: {
+ text: "mixedbread.com",
+ url: "https://www.mixedbread.com/dashboard/api-keys",
+ },
+ },
+ ],
+
+ testConfig: {
+ getTestFunction: async () => {
+ const { testMixedbread } = await import("./test");
+ return testMixedbread;
+ },
+ },
+
+ actions: [
+ {
+ slug: "ingest-file",
+ label: "Ingest File",
+ description: "Upload a file to a Mixedbread document store",
+ category: "Mixedbread",
+ stepFunction: "mixedbreadIngestFileStep",
+ stepImportPath: "ingest-file",
+ outputFields: [
+ { field: "fileId", description: "The ID of the uploaded file" },
+ { field: "status", description: "The status of the upload" },
+ ],
+ configFields: [
+ {
+ key: "storeIdentifier",
+ label: "Store Identifier",
+ type: "template-input",
+ placeholder: "my-store or {{NodeName.storeId}}",
+ example: "my-document-store",
+ required: true,
+ },
+ {
+ key: "externalId",
+ label: "External ID",
+ type: "template-input",
+ placeholder: "unique-id or {{NodeName.id}}",
+ example: "doc-123",
+ required: true,
+ },
+ {
+ key: "content",
+ label: "Content",
+ type: "template-textarea",
+ placeholder: "File content or {{NodeName.content}}",
+ example: "Document content here...",
+ required: true,
+ rows: 5,
+ },
+ {
+ key: "mimetype",
+ label: "MIME Type",
+ type: "template-input",
+ placeholder: "text/plain or {{NodeName.mimetype}}",
+ example: "text/plain",
+ defaultValue: "text/plain",
+ required: true,
+ },
+ {
+ key: "metadata",
+ label: "Metadata (JSON)",
+ type: "template-textarea",
+ placeholder: '{"key": "value"} or {{NodeName.metadata}}',
+ example: '{"source": "workflow", "category": "docs"}',
+ rows: 3,
+ },
+ ],
+ },
+ ],
+};
+
+registerIntegration(mixedbreadPlugin);
+export default mixedbreadPlugin;
diff --git a/plugins/mixedbread/steps/ingest-file.ts b/plugins/mixedbread/steps/ingest-file.ts
new file mode 100644
index 00000000..281d6116
--- /dev/null
+++ b/plugins/mixedbread/steps/ingest-file.ts
@@ -0,0 +1,113 @@
+import "server-only";
+
+import { fetchCredentials } from "@/lib/credential-fetcher";
+import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
+import type { MixedbreadCredentials } from "../credentials";
+
+const MIXEDBREAD_API_URL = "https://api.mixedbread.com/v1";
+
+type IngestFileResult =
+ | { success: true; fileId: string; status: string }
+ | { success: false; error: string };
+
+export type MixedbreadIngestFileCoreInput = {
+ storeIdentifier: string;
+ externalId: string;
+ content: string;
+ mimetype: string;
+ metadata?: string;
+};
+
+export type MixedbreadIngestFileInput = StepInput &
+ MixedbreadIngestFileCoreInput & {
+ integrationId?: string;
+ };
+
+async function stepHandler(
+ input: MixedbreadIngestFileCoreInput,
+ credentials: MixedbreadCredentials
+): Promise {
+ const apiKey = credentials.MIXEDBREAD_API_KEY;
+
+ if (!apiKey) {
+ return {
+ success: false,
+ error:
+ "MIXEDBREAD_API_KEY is not configured. Please add it in Project Integrations.",
+ };
+ }
+
+ let parsedMetadata: Record | undefined;
+ if (input.metadata) {
+ try {
+ parsedMetadata = JSON.parse(input.metadata);
+ } catch {
+ return {
+ success: false,
+ error: "Invalid JSON in metadata field",
+ };
+ }
+ }
+
+ try {
+ const fileBlob = new Blob([input.content], { type: input.mimetype });
+ const formData = new FormData();
+
+ const params: Record = {
+ external_id: input.externalId,
+ };
+ if (parsedMetadata) {
+ params.metadata = parsedMetadata;
+ }
+ formData.append("params", JSON.stringify(params));
+ formData.append("file", fileBlob, input.externalId);
+
+ const response = await fetch(
+ `${MIXEDBREAD_API_URL}/stores/${encodeURIComponent(input.storeIdentifier)}/files/upload`,
+ {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${apiKey}`,
+ },
+ body: formData,
+ }
+ );
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ return {
+ success: false,
+ error: `HTTP ${response.status}: ${errorText}`,
+ };
+ }
+
+ const result = await response.json();
+
+ return {
+ success: true,
+ fileId: result.id,
+ status: result.status,
+ };
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ return {
+ success: false,
+ error: `Failed to ingest file: ${message}`,
+ };
+ }
+}
+
+export async function mixedbreadIngestFileStep(
+ input: MixedbreadIngestFileInput
+): Promise {
+ "use step";
+
+ const credentials = input.integrationId
+ ? await fetchCredentials(input.integrationId)
+ : {};
+
+ return withStepLogging(input, () => stepHandler(input, credentials));
+}
+mixedbreadIngestFileStep.maxRetries = 0;
+
+export const _integrationType = "mixedbread";
diff --git a/plugins/mixedbread/test.ts b/plugins/mixedbread/test.ts
new file mode 100644
index 00000000..0875d87e
--- /dev/null
+++ b/plugins/mixedbread/test.ts
@@ -0,0 +1,36 @@
+export async function testMixedbread(credentials: Record) {
+ try {
+ const apiKey = credentials.MIXEDBREAD_API_KEY;
+
+ if (!apiKey) {
+ return {
+ success: false,
+ error: "MIXEDBREAD_API_KEY is required",
+ };
+ }
+
+ // Make a lightweight API call to validate the key
+ const response = await fetch("https://api.mixedbread.com/v1/stores", {
+ method: "GET",
+ headers: {
+ Authorization: `Bearer ${apiKey}`,
+ },
+ });
+
+ if (response.ok) {
+ return { success: true };
+ }
+
+ if (response.status === 401) {
+ return { success: false, error: "Invalid API key" };
+ }
+
+ const error = await response.text();
+ return { success: false, error: `API error: ${error}` };
+ } catch (error) {
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : String(error),
+ };
+ }
+}