Skip to main content
The @team-plain/graphql package provides a fully typed client for Plain’s GraphQL API. It is auto-generated from the GraphQL schema, which means every query and mutation available in the API is available in the SDK. You can use any GraphQL client to interact with Plain’s API, but this SDK gives you type safety, automatic pagination, and structured error handling out of the box.

Installation

npm install @team-plain/graphql
Supports both ESM and CJS.

Setup

import { PlainClient } from "@team-plain/graphql";

const client = new PlainClient({ apiKey: "plainApiKey_xxx" });
You will need an API key — see authentication for how to create one.

Queries

Queries are available under client.query. Relations on returned models are lazy-loaded — accessing them triggers a separate API call automatically.
const customer = await client.query.customer({ customerId: "c_123" });
console.log(customer.fullName);

// Relations are lazy-loaded — accessing them makes a separate API call
const company = await customer.company;
console.log(company.name);

Mutations

Mutations are available under client.mutation. Mutation errors are returned as typed data, not thrown as exceptions. This matches Plain’s API where all mutations return *Output types with an optional error field.
const result = await client.mutation.upsertCustomer({
  input: {
    identifier: { emailAddress: "alice@example.com" },
    onCreate: {
      fullName: "Alice",
      email: { email: "alice@example.com", isVerified: false },
    },
    onUpdate: {},
  },
});

if (result.error) {
  // Typed MutationError with message, type, code, and field-level errors
  console.error(result.error.message);
  result.error.fields?.forEach((f) => {
    console.error(`  ${f.field}: ${f.message}`);
  });
} else {
  console.log(result.customer?.id);
}

Pagination

const customers = await client.query.customers({ first: 10 });

for (const customer of customers.nodes) {
  console.log(customer.fullName);
}

// Fetch the next page
const nextPage = await customers.fetchNext();

Union types

GraphQL union and interface fields are exposed as discriminated unions of model classes. Each union member has a __typename property for type narrowing and supports the same lazy-loading as any other model.
const thread = await client.query.thread({ threadId: "t_123" });

// Narrow with __typename
if (thread.createdBy.__typename === "UserActor") {
  console.log(thread.createdBy.userId);

  // Lazy-load a relation on the union member
  const user = await thread.createdBy.user;
  console.log(user?.fullName);
}

// Or narrow with instanceof
import { UserActorModel } from "@team-plain/graphql";

if (thread.createdBy instanceof UserActorModel) {
  const user = await thread.createdBy.user;
}
Models also support querying sub-connections directly:
const thread = await client.query.thread({ threadId: "t_123" });

// Fetch timeline entries directly from the thread model
const timelineEntries = await thread.timelineEntries({ first: 25 });

for (const entry of timelineEntries.nodes) {
  console.log(entry.entry.__typename);
}

Error handling

  • Queries: network, auth (401), forbidden (403), and rate limit (429) errors throw typed exceptions (AuthenticationError, ForbiddenError, RateLimitError, NetworkError, PlainGraphQLError).
  • Mutations: return the full *Output type. Check result.error for a typed MutationError with message, type, code, and fields[]. This is intentional — Plain’s API treats mutation errors as data.
For more details on error handling patterns, see error handling.

Migrating from @team-plain/typescript-sdk

If you’re upgrading from the old @team-plain/typescript-sdk package, see the migration guide for a full breakdown of breaking changes.

Resources