# Use custom objects with Stripe Apps and Workflows

Build Dashboard pages for your records with Stripe Apps, and automate work on them with Workflows.

Use custom objects when you want your team to be able to see them and act on them in the Dashboard. You can also use them when you want to increase automation. Two Stripe capabilities work directly with your objects: [UI extensions](https://docs.stripe.com/stripe-apps/how-ui-extensions-work.md), which build custom interfaces in the Dashboard, and [Workflows](https://docs.stripe.com/workflows.md), which automate work without code.

## Build a custom UI with UI extensions

A Stripe app UI extension is a React component that Stripe renders in the Dashboard. Your app’s UI can read and write your records, so your team can work with them without leaving the Dashboard. A [viewport](https://docs.stripe.com/stripe-apps/reference/viewports.md) determines where your UI appears:

- **A full-page app**: Gives your app its own Dashboard page, where you can build a dashboard for your custom objects.
- **A drawer view**: Opens next to a Dashboard page, such as a customer’s page.

### Build a Dashboard page with a full-page app

A [full-page app](https://docs.stripe.com/stripe-apps/patterns/full-page-apps.md) has an entire Dashboard page at `dashboard.stripe.com/app/<app_id>`. After you [upload and install](https://docs.stripe.com/stripe-apps/upload-install-app.md) your app, your team opens it from **Apps** in the Dashboard navigation. Use it to build a dashboard for your custom objects. This includes an overview with counts and charts, lists that your team filters, a page for each record, and forms that create and update records. The project that `stripe generate app` creates includes a full-page view in its `ui/` folder, registered in `stripe-app.yaml`:

```yaml
ui_extension:
  views:
    - viewport: stripe.dashboard.fullpage
      component: FullpageView
```

Build the page with Stripe Apps components:

- [FullPageView](https://docs.stripe.com/stripe-apps/components/fullpageview.md) renders the page header with your app’s name and icon.
- [Routing](https://docs.stripe.com/stripe-apps/routing.md) gives each view a URL, such as `/tickets/objrec_...` for a record, that your team can bookmark and share.
- [OverviewPage](https://docs.stripe.com/stripe-apps/components/overviewpage.md) lays out summary metrics and charts.
- [DataTable](https://docs.stripe.com/stripe-apps/components/datatable.md) lists records with status cells, pagination, and row actions.
- [DetailPage](https://docs.stripe.com/stripe-apps/components/detailpage.md) shows a single record with breadcrumbs and page actions.
- [FocusView](https://docs.stripe.com/stripe-apps/components/focusview.md) holds forms that create and edit records.
- [Route descriptors](https://docs.stripe.com/stripe-apps/route-descriptors.md) link to Dashboard pages, such as the customer that a record references.

Full-page apps require `@stripe/ui-extension-sdk` 9.2.1 or later.

#### Example: a support desk in the Dashboard

Your support team works through `support_ticket` records in a full-page app instead of a separate tool:

- An **Overview** tab counts open and in-progress tickets.
- A **Queue** tab lists tickets in a table, with a status filter that changes the search query.
- Selecting a ticket opens its page, with the ticket’s fields and a link to the customer’s page in the Dashboard.
- **Mark resolved** updates the ticket’s status. A workflow that starts when a ticket is updated can take the next step, such as emailing a team member.

### Read and write records from your UI

Call the `/v2/extend/objects/<plural_name>` API with `createHttpClient()` and `makeRequest()` from the [UI extension SDK](https://docs.stripe.com/stripe-apps/reference/extensions-sdk-api.md). Stripe’s standard SDKs don’t include custom object endpoints.

```typescript
import {
  createHttpClient,
  AUTHORIZATION_HEADER,
  AUTHORIZATION_VALUE,
} from '@stripe/ui-extension-sdk/http_client';

const httpClient = createHttpClient();

async function request(method: 'GET' | 'POST', path: string, body?: object) {
  const response = await httpClient.makeRequest(
    'api.stripe.com',
    '443',
    path,
    method,
    {
      [AUTHORIZATION_HEADER]: AUTHORIZATION_VALUE,
      'Stripe-Version': '2026-10-28.preview',
      'Stripe-Custom-Object-Versions': 'support_tickets@0.0.1',
      ...(body && {'Content-Type': 'application/json'}),
    },
    body ? JSON.stringify(body) : null,
    'https',
  );
  const json = await response.toJSON();
  if (response.getStatusCode() >= 400) {
    throw new Error(`Request failed with status ${response.getStatusCode()}`);
  }
  return json;
}

// Retrieve a ticket
export function retrieveTicket(id: string) {
  return request('GET', `/v2/extend/objects/support_tickets/${id}`);
}

// Update a ticket's status. Fields you leave out keep their values.
export function updateTicketStatus(id: string, status: string) {
  return request('POST', `/v2/extend/objects/support_tickets/${id}`, {
    fields: {status},
  });
}
```

Keep the following in mind:

- **Authentication**: Pass `AUTHORIZATION_VALUE` in the Authorization header, not an API key. The Dashboard sends the request with the signed-in team member’s session.
- **Headers**: Send the same headers as any record request: `Stripe-Version`, and `Stripe-Custom-Object-Versions` on every request except deletes. Name the version your UI was built for, and change it when your UI starts using fields from a newer version (see [Version your custom objects](https://docs.stripe.com/custom-objects/versioning.md)).
- **Errors**: `makeRequest()` also resolves for error responses. Check `getStatusCode()` before you use the body.
- **After an update**: Show the record from the response, or retrieve it by ID. Search results can lag behind writes (see [Search records](https://docs.stripe.com/custom-objects/search.md)).

### UI extensions during private preview

During private preview, these conditions apply:

- **Use a private app**: Read and write custom objects from a [private app](https://docs.stripe.com/stripe-apps/distribution-options.md) that’s installed in your own account. Your app’s UI doesn’t need custom object permissions in its manifest, so leave them out.
- **Where it renders**: There’s no viewport for the Dashboard’s custom object pages, so a view can’t receive the record a user is viewing. Render your UI in a full-page app or a drawer view, and read or write records from there.
- **Dashboard pages**: UI extensions are separate from the **More** > **Custom objects** pages, which Stripe provides and which are read-only (see [Manage custom objects in the Dashboard](https://docs.stripe.com/custom-objects/dashboard.md)).
- **Local development**: During `stripe apps start`, custom object requests from your UI return a 403 error. Upload your app and test the installed version.

## Automate with Workflows

Workflows work with your custom objects:

- **Trigger on record changes**: A workflow can start when you create or update a record of a specific object definition. After the [trigger](https://docs.stripe.com/workflows/triggers.md), later steps can check the record’s current field values in conditions.
- **Create and update records**: Each object definition adds steps to the workflow builder, such as **Create a Custom Shipment**, so a workflow can create and update records.
- **Act on referenced Stripe objects**: When a field is a reference, such as `Ref<PaymentIntent>`, later steps can use it as that Stripe object, for example to refund the payment a record references. A field that stores an ID as a plain string doesn’t work this way.
- **Pinned versions**: All custom object steps and triggers from the same app in a workflow share one version of that app’s definitions. Stripe selects the latest version when you add the first step or trigger. They keep using that version after you publish a new one. To use a newer version, remove every custom object step and trigger from that app in the workflow, then add them again.

A workflow that starts when a record is updated runs on every update while its conditions match, including updates that don’t change the field your condition checks. Make sure its steps are safe to repeat. For example, after a workflow refunds a deposit, have it update a field on the record, and check that field in the condition.

### Workflows during private preview

During private preview, these conditions apply to Workflows:

- Workflows don’t have a step that searches for records or filters them by field value.
- A workflow that starts when a record is deleted can’t retrieve the record, so its run is blocked and its steps don’t run. The same happens when a record is deleted before a workflow that it started retrieves it.
- Running a workflow manually fails with a 403 error when it has custom object steps. Workflows that start from events aren’t affected.
- `Currency` fields appear as a plain list of currency codes, and monetary amount fields don’t link the amount to its currency.

## Use them together

Your custom objects sit next to your Stripe data and work across Stripe. A shipment record you create through the API appears in the Dashboard, can show up in a full-page app for your team, can reference the invoice it belongs to, and can start a workflow when it changes, without a separate database or a sync job.
