Export Records as PDF

Export workspace records to a single PDF rendered from a Portable Document template and delivered by email, with optional filters and real-time progress.


Use the exportRecordsPdf mutation to export the records of a workspace as one PDF, rendered from a Portable Document template. The mutation is asynchronous: it returns true once the job is queued, the finished PDF is emailed to the requesting user, and progress streams over the subscribeToImportExportProgress subscription. Records are Record objects, a workspace is a Workspace, a record table is a RecordDatabase, and a template is a PortableDocument in the API.

mutation ──► export-pdf queue ──► worker renders one PDF (one page set per record)
                                      │
                                      ├─► progress events ──► subscription + exportPdfProgress query
                                      └─► uploaded file ──► "Download PDF" email

Request

Export every record of a workspace through a template:

mutation ExportRecordsPdf {
  exportRecordsPdf(input: { projectId: "project_123", pdfId: "clm4n8qwx000008l0g4oxdqn7" })
}

projectId must be the workspace ID — a slug is not accepted and fails with PROJECT_NOT_FOUND. pdfId is the template’s id (the CUID returned by the portableDocuments query), not its pdf_… uid. The template must be active and belong to that workspace.

Parameters

ExportRecordsPdfInput

ParameterTypeRequiredDescription
projectIdString!YesID of the workspace to export records from. Must be the workspace ID — a slug is not accepted.
pdfIdString!YesID of the Portable Document template to render each matching record with. Must be active, and belong to the workspace.
databaseIdStringNoExport only this record table of the workspace. Must be a live table in that workspace, and the template must be linked to it (below). If omitted, every live table of the workspace is exported.
filterRecordsFilterNoCriteria to limit which records are exported. If omitted, all records are exported.

A template fills its overlay placements from a table’s fields through the link created with setPortableDocumentDatabaseLinks. With databaseId set, the template must have a link to that table, and the mutation validates the link against the live template placements and the table fields you can see before queueing the job. A template that is not linked to the selected table, or a mapping that no longer fits, fails with BAD_USER_INPUT before the job is queued. See Link a template to record tables for the mapping rules and the field types the renderer covers.

With databaseId omitted, each matching record still renders through the table it was selected from, using that table’s link when the template has one.

RecordsFilter

Combine any of these fields to narrow the export. Unless noted, fields are ANDed together.

ParameterTypeRequiredDescription
companyIds[String!]!YesOrganization IDs that scope the filter. Required by the type even though the workspace is already given by projectId.
todoIds[String!]NoExport only these specific record IDs.
assigneeIds[String!]NoInclude records assigned to any of these users.
assigneeMatchAssigneeMatchNoHow multiple assigneeIds combine — ANY (default) or ALL.
hasAnyAssigneeBooleanNotrue → at least one assignee; false → none.
unassignedBooleanNoWhen true, include only records with no assignee.
tagIds[String!]NoInclude records carrying any of these tag IDs.
tagColors[String!]NoInclude records carrying a tag of any of these hex colors.
tagTitles[String!]NoInclude records carrying a tag with any of these titles.
todoListIds[String!]NoInclude records in any of these lists (by ID).
todoListTitles[String!]NoInclude records in any of these lists (by title).
doneBooleanNoFilter by completion status (true = completed).
showCompletedBooleanNoInclude completed records in the result set.
startedAtDateTimeNoInclude records started on or after this timestamp.
duedAtDateTimeNoInclude records due on or before this timestamp.
dueStart / dueEndDateTimeNoDue-date range start and end.
duedAtStart / duedAtEndDateTimeNoAlternative due-date range start and end.
createdStart / createdEndDateTimeNoInclude records created on or after / on or before this timestamp.
createdByIds[String!]NoInclude records created by any of these users.
updatedAt_gt / updatedAt_gteDateTimeNoInclude records updated after / at or after this timestamp.
archivedBooleanNoArchive scope. Omitted or false → active records only; true → archived records only.
searchStringNoFull-text search query.
qStringNoQuick search query (matches the record title).
recordNameStringNoRecord-title substring search. Prefer over q for name-only search.
recordNameOpFilterComparisonOperatorNoComparison operator applied to recordName (CONTAINS, NOT_CONTAINS, IS, …).
notAssigneeIds[String!]NoExclude records assigned to any of these users.
notTagIds[String!]NoExclude records carrying any of these tag IDs.
notColors[String!]NoExclude records of any of these colors.
notTodoListIds[String!]NoExclude records in any of these lists.
hasTag / hasColor / hasDueDate / hasDescription / hasChecklist / hasDependency / hasReferenceBooleanNoExistence filters; when true, include only records that have the named attribute.
fieldsJSONNoCustom-field filter conditions. See custom-field filters.
opFilterLogicalOperatorNoLogical operator applied to the fields conditions. Defaults to AND.
groups[RecordFilterGroupInput!]NoNested filter groups. When provided, the flat fields above are ignored in favor of the groups.
groupLinks[FilterLogicalOperator!]NoOperators linking adjacent groups; length should be groups.length - 1. If shorter, the last operator is repeated.
projectIds[String!]NoNo effect here — the PDF export covers only the one workspace named by projectId.
excludeArchivedProjectsBooleanNoNo effect here — the export is scoped to the one workspace.
rankBeforeRecordRankCursorInputNoKeyset cursor for paginated list reads. No effect on the export.
coordinatesJSONNoMap-view bounding box. No effect on the export.

Filter conditions resolve against each exported table’s mounted fields, so a condition on a custom field matches the records of every table that mounts a field it resolves to.

Custom-field filters

Each entry in the fields array must include a type — the query layer routes on it, and entries without type are silently dropped. The most common type is CUSTOM_FIELD:

[
  {
    "type": "CUSTOM_FIELD",
    "customFieldId": "field_123",
    "customFieldType": "CHECKBOX",
    "op": "IS",
    "values": true
  }
]

Other valid type values are TIME_IN_LIST, RELATIVE_DUEDATE, and FIELD. The operators available per customFieldType depend on the field type.

Response

exportRecordsPdf returns a bare boolean — true once the job is queued. The PDF is generated in the background and emailed to the requesting user.

{ "data": { "exportRecordsPdf": true } }

Returns

FieldTypeDescription
exportRecordsPdfBoolean!true when the export job has been queued.

Full example

Export the records of one table — incomplete, assigned to two users, tagged, due in Q1, matching a search term and a custom-field value — through a linked template:

mutation ExportFilteredRecordsPdf {
  exportRecordsPdf(
    input: {
      projectId: "project_123"
      pdfId: "clm4n8qwx000008l0g4oxdqn7"
      databaseId: "clm4n8qwx000008l0t9table1"
      filter: {
        companyIds: ["company_123"]
        done: false
        assigneeIds: ["user_123", "user_456"]
        tagIds: ["tag_123"]
        dueStart: "2026-01-01T00:00:00Z"
        dueEnd: "2026-03-31T23:59:59Z"
        q: "launch"
        fields: [
          {
            type: "CUSTOM_FIELD"
            customFieldId: "field_123"
            customFieldType: "SELECT_SINGLE"
            op: "IS"
            values: ["option_123"]
          }
        ]
        op: AND
      }
    }
  )
}

Which records are selected

  • With databaseId — only that table’s records are considered.
  • Without databaseId — every live table of the workspace is walked in table order. Each matching record prints through the table it was selected from, and a record that sits in several tables prints once.

The selection runs through the exporting user’s live record and field policy: a record, or a field on a record, that your role cannot view is left out of the PDF. An empty result fails the job — there is no “0 pages” PDF (see Job-time failures).

Delivery

The worker uploads the finished PDF and publishes a COMPLETE progress event carrying the download URL, then emails the requesting user a Download PDF link for the file. The URL is a signed export link, so it expires; the email is the durable delivery, and the same URL is available from the progress channel below.

Track export progress

Subscribe to subscribeToImportExportProgress to receive real-time progress for the current user’s imports and exports. The subscription filters server-side by userId, so pass the ID of the user who started the export.

subscription TrackPdfExportProgress {
  subscribeToImportExportProgress(projectId: "project_123", userId: "user_123")
}

PDF export events carry type: "EXPORT_PDF":

{
  "data": {
    "subscribeToImportExportProgress": {
      "type": "EXPORT_PDF",
      "status": "IN_PROGRESS",
      "userId": "user_123",
      "projectId": "project_123",
      "projectName": "Q1 Launch",
      "current": 120,
      "total": 250,
      "progress": 48
    }
  }
}
FieldDescription
typeAlways "EXPORT_PDF" for this export.
statusIN_PROGRESS while rendering, COMPLETE with fileUrl, or ERROR with an error message.
projectIdThe workspace ID you passed to the mutation.
current / total / progressRecords rendered so far, total records, and the percentage. ERROR events omit them.
fileUrlDownload URL of the finished PDF — present only on COMPLETE.

Live events are fire-and-forget: a client that navigates away or refreshes misses everything sent in between. The exportPdfProgress query reads the durable server-side snapshot (kept for about 1 hour, scoped to the calling user) to re-sync on mount:

query ExportPdfProgress {
  exportPdfProgress(projectId: "project_123")
}
{
  "data": {
    "exportPdfProgress": {
      "status": "COMPLETE",
      "current": 250,
      "total": 250,
      "fileUrl": "https://api.blue.app/exports/eyJ...",
      "updatedAt": "2026-05-29T14:03:02.000Z"
    }
  }
}

The snapshot’s status is GENERATING, COMPLETE, or ERROR; on ERROR it carries error instead of fileUrl.

Errors

CodeWhen
PROJECT_NOT_FOUNDThe workspace does not exist, projectId is a slug instead of the ID, or the requesting user has no membership row in it. databaseId raises the same code when it is not a live table in that workspace.
PORTABLE_DOCUMENT_NOT_FOUNDNo template with that pdfId in the workspace — an inactive template is indistinguishable from a missing or foreign one.
BAD_USER_INPUTThe template is not linked to the selected databaseId, or the link fails validation: a placement has no mapping, the mapped field is not visible to you on that table, or its type does not fit the placement.
RATE_LIMITEDMore than one exportRecordsPdf request within 50 seconds by the same token. The 50-second window is per operation, so it is separate from exportRecords’ own window.

Job-time failures

Everything above rejects the mutation itself. The queued job can still fail; the failure surfaces as an ERROR progress event plus a failure email to the requesting user, never as a GraphQL error:

SituationWhat the user is told
The filter matched 0 records.No records matched your active filters, so there was nothing to export.
The filter matched more than 250 records.The count and the cap, with the advice to narrow the filter or use CSV export.
The template was deleted or deactivated meanwhile.A generic failure message.
A mapped field is no longer mounted on the table.A generic failure message.
A filter condition the record engine does not support.A generic failure message.

Permissions

CheckRequirement
Field permissionProject-level OWNER, ADMIN, or MEMBER on an active workspace.
ResolverThe caller must have a membership row in the workspace (part of the project lookup).
WorkerThe selection applies the exporting user’s record and field visibility, so the PDF shows only what that user can see.

There is no batch or multi-workspace PDF export — one call covers one workspace.

Notes

  • One PDF, not one per table. Unlike the CSV export, which builds a file per database, exportRecordsPdf appends every matching record’s pages to a single document.
  • The template must be built first. Create and place fields on the template as described in Build Portable Document templates, and print a single record with printPortableDocument to check the layout before exporting hundreds of records.
  • Rate limit and tier limits stack. The per-operation 1-per-50-seconds limit runs inside GraphQL on top of the tier limits described in Rate Limits.
  • Imports and CSV exports are covered elsewhere. See the Import/Export overview and Export records.