Recipe: Print Ticket History app for App Builder | The place for Zendesk users to come together and share
Skip to main content
Vishal Jethani
Product Manager
June 30, 2026

Recipe: Print Ticket History app for App Builder

  • June 30, 2026
  • 0 replies
  • 106 views

Problem Statement

The Print Ticket History app provides bulk ticket export and print functionality directly within Zendesk Support. It allows agents and admins to search for tickets by user criteria, select individual or multiple tickets, and export them as a PDF, with an option to hide internal notes from the printed output.

 

Location: nav bar.

 

Prompt

Create a `nav_bar` app called "Print Ticket History" that lets agents search Zendesk Support tickets by requester (name or email), select a batch with checkboxes, and download those tickets as PDFs (a single PDF for one ticket, a ZIP archive for multiple). Match the exact behaviours, layout, and PDF format described below — earlier builds got several details wrong, so the wording here is deliberately strict.

 

**App settings**

1. Add exactly one install-time parameter in `manifest.json`:

   ```json

   {

     "name": "hide_internal_comments",

     "type": "checkbox",

     "default": false,

     "required": false

   }

   ```

   The `type` MUST be `checkbox` — never `text`, `string`, `boolean`, or any free-text input. The admin's install screen must render a single tickable checkbox labelled "Hide private comments when printing" with help text "Select to hide any private comments in the Internal note field when printing tickets." A free-text input here is a bug.

2. Read this setting via `const metadata = await zafClient.metadata()` and use `metadata.settings.hide_internal_comments` (boolean). Do **not** use `zafClient.get('setting.hide_internal_comments')` — that path does not exist and will throw `APIUnavailable` at runtime.

 

**Layout overview**

3. The whole app is a single screen split into three stacked regions, each spanning the full width of the iframe with ~16px outer padding: a search bar (top), a status/action bar (middle), and a results table with a pagination footer (bottom). Use Garden components throughout. The app does not need to be responsive beyond filling its iframe.

 

**Search bar (top)**

4. Render a single horizontal flex row containing a Garden `Input` and a primary blue Garden `Button` labelled "Search". The `Input` MUST take `flex: 1` and visibly fill all remaining horizontal space — do **not** wrap it in a fixed-width `Field`, do **not** apply a `width` of any value (e.g. `240px`, `50%`), and do **not** put it inside a small Garden field group. The Search button keeps its natural width on the right with ~8–12px gap. Verify visually that the input edge-to-edge consumes the full row minus the button.

5. **Do NOT render a "Search tickets" (or any other) label above or beside the input.** The input must stand alone with only its placeholder text visible — earlier builds added a Garden `<Label>Search tickets</Label>` above the input and it must be removed. If a label is required by Garden's `Field` structure, use `<Label hidden>.</Label>` (visually hidden, single dot) so no text renders. The accessible name, if needed, can come from `aria-label` on the input.

6. The input has placeholder "Search by name or email (one or more separated by English commas)" and auto-focuses on app open.

7. Pressing Enter or clicking Search triggers the search. Disable both controls while a search is in flight.

8. If the input is empty on submit, show `zafClient.invoke('notify', 'A name or an email address is required to perform the search.', 'alert')` and skip the API call.

 

**Search behaviour**

8. Split the input on commas, trim each fragment, and build a query of space-separated `requester:<term>` clauses (e.g. `requester:alice@example.com requester:bob`).

9. Call `zafClient.request({ url: '/api/v2/search.json?page=1&query=type:ticket order_by:created sort:desc <requesterQuery>', type: 'GET', dataType: 'json' })`. Read `response.count` and fetch additional pages in parallel (100 results per page) up to a hard cap of 5 pages (500 tickets total). Concatenate all `results` arrays. If `next_page` is null on page 1, do not fetch more pages.

10. Collect every distinct `requester_id`, `assignee_id`, and `submitter_id` from the returned tickets. Fetch user details in batches of up to 100 via `/api/v2/users/show_many.json?ids=<comma-separated>`. For any IDs missing from the response (deleted users), fall back to `/api/v2/users/{id}.json` for each.

11. Fetch all groups via `/api/v2/groups.json` to map `group_id → name`.

12. Build a row object per ticket containing: `id`, `subject` (truncated to 256 chars + "…" if longer), `requested` ("MMM DD, YYYY" from `created_at`), `updated` ("MMM DD, YYYY" from `updated_at`), `requester` (user's name), `requesterEmail`, `group` (group name or "-"), `status` (uppercase first letter only — "N", "O", "P", "H", "S", "C"), `status_style` (raw status string — `new`, `open`, `pending`, `hold`, `solved`, `closed` — used for colour), `channel` (`ticket.via.channel`), `type`, `priority`, `description`, `previewDescription` (truncated description), `photoUrl` (the requester's photo URL or a default avatar). Keep all rows in client memory after the search finishes — pagination is purely client-side.

 

**Status & action bar (middle)**

13. A single horizontal bar below the search input with a thin bottom border and ~12–16px vertical padding. Left: a count message. Right: an "Include custom fields" toggle, a "Clear selection" outline button, and (when ≥1 ticket is selected) a primary blue "Print N tickets" button. Items on the right are spaced ~12px apart.

14. Count message logic:

    - 0 results → "No tickets found"

    - 1 result → "1 ticket found"

    - more than 1 → "{count} tickets found"

    - while a search is loading → hide the count and centre a Garden `Inline` spinner instead.

15. The "Include custom fields" toggle and "Clear selection" button are disabled until ≥1 ticket is selected. Clicking "Clear selection" deselects all rows on the current page and resets the toggle's effect on selection state.

16. Print button label:

    - "Print 1 ticket" when exactly one is selected

    - "Print N tickets" otherwise (substitute the actual count)

    - While generating, replace the label with a small spinner and disable the button.

17. Hard cap of 30 selected tickets per export. If the user clicks Print with more than 30 selected, show `zafClient.invoke('notify', 'You can only select up to 30 tickets to export.', 'error')` and abort the export.

 

**Results table (bottom)**

18. Render results in a Garden `Table` with these columns and approximate widths: select checkbox (≈3%), status badge (≈3%), ID (≈6%), Subject (≈38%), Requested (≈12%), Updated (≈12%), Requester (≈14%), Group (≈12%). Use Garden's default `Table` density — do **not** use the `isCompact` / `size="small"` variant.

19. **Spacing & density.** The table must feel roomy, not dense. Use these minimums:

    - Row height: 48–56px

    - Vertical cell padding: 12–14px

    - Horizontal cell padding: 12–16px

    - Body font size: 14px

    - Header font size: 13px, weight 600

    - 8–12px vertical breathing room around the entire table block

    Earlier builds rendered the table tightly packed; if the rendered rows look cramped or the text feels squeezed, adjust padding upward until each row clearly stands apart from its neighbours.

20. **Row selection — must use real checkboxes.** The first column of every body row is a Garden `Checkbox` (inside a `Field`) bound to that row's `ticketSelected` boolean. The header row's first cell is also a real Garden `Checkbox` that selects/deselects every row on the current page; show it `indeterminate` when some-but-not-all rows on the current page are selected, `checked` when all are selected, `unchecked` when none are. **Do NOT add an `onClick` on the `<Row>` itself that toggles selection** — earlier builds did this and it produced confusing behaviour (clicking a status badge or date cell selected the row, while the checkbox visually appeared inert). The checkbox is the one and only interaction that toggles selection.

21. **Bare checkboxes — pass a single dot `.` as the label, rendered with `color: transparent` so it's invisible.** Garden's `Checkbox` requires a `<Label>` child for proper structure, so the label must exist — but its visible text MUST be a single hardcoded dot character `"."` styled with `color: transparent` (not an empty string `""`, not a space `" "`, not the ticket ID, not "Select ticket {id}", not "Select all tickets", not any other visible text). The dot keeps the markup well-formed; the transparent colour hides it against any background. Example:

    ```jsx

    <Field>

      <Checkbox checked={...} onChange={...}>

        <Label style={{ color: 'transparent' }}>.</Label>

      </Checkbox>

    </Field>

    ```

    Apply the exact same pattern (single transparent dot) to the header select-all checkbox and to every per-row checkbox. Do this consistently on every checkbox in the app. If you need an accessible name for screen readers, set it on the input via `aria-label` (e.g. `aria-label="Select ticket {id}"`) — never put readable text inside `<Label>`. Earlier attempts (empty string, ticket ID, single space, plain visible dot) all either broke layout or leaked visible text.

22. Selected rows get a light-blue background tint — `#cfeafb` at ~40% opacity (equivalent to `#cfeafb66`).

23. The status column is a small filled circle (~22px diameter) showing the first letter of the status (N, O, P, H, S, C) in white. Colour by `status_style`: New = yellow `#ffd424`, Open = red `#d93f4c`, Pending = blue `#3091ec`, On-hold = black `#222`, Solved = grey `#87929d`, Closed = grey `#87929d`. Show the full status name as a `title` tooltip on hover.

24. The ID cell renders as `#{id}` styled like a blue underlined-on-hover anchor. Clicking calls `zafClient.invoke('routeTo', 'ticket', ticketId)` to open the ticket in Zendesk. Stop event propagation so the click doesn't also flip a sibling checkbox.

25. The Subject cell shows the truncated subject. On hover, show a small popover anchored to the row that contains: the requester's avatar, the full subject in bold, the requester's name, a coloured status pill with the full status text, "Ticket #{id}" in small text, and the truncated description preview. Anchor the popover above the row for rows in the upper half of the page and below for rows in the lower half so it never clips outside the iframe.

 

**Pagination footer (below the table)**

26. **Use Garden's numbered `<Pagination>` component.** Import it from `@zendeskgarden/react-pagination` and render numbered page buttons followed by previous/next chevrons (e.g. `1 2 3 >`, or `1 ... 4 5 6 >` when there are many pages). Do **not** roll your own pagination control with custom buttons, do **not** render a "Page N of M" text label, and do **not** show only a single page-size selector with no page numbers — earlier builds did all three and produced an unusable footer.

27. Show pagination only when search results exist AND `totalResults > ticketsPerPage` (i.e. there is genuinely more than one page).

28. Wire the props as: `totalPages={Math.ceil(totalResults / ticketsPerPage)}`, `currentPage={currentPage}` (1-based), `onChange={(pageNum) => goToPage(pageNum)}`. The `goToPage` handler MUST work like this — earlier builds had a broken handler that left the page on `1` no matter what was clicked:

    - clamp `pageNum` to `[1, totalPages]` (default to 1 if out of range),

    - compute `startIndex = (pageNum - 1) * ticketsPerPage`,

    - slice `allTickets.slice(startIndex, startIndex + ticketsPerPage)` into `currentPageTickets`,

    - set `currentPage = pageNum`,

    - clear selection (`selectedTickets = []`, `selectAllTickets = false`).

    Verify in testing that clicking page `2` advances to tickets `31–60`, that clicking the `>` chevron advances by one page, and that clicking page `3` lands on tickets `61–90`. Pagination is purely client-side — no extra API calls per page change.

29. To the right of the page-number controls (in the same footer row), show "Number of tickets per page:" followed by a small numeric Garden `Input` bound to `ticketsPerPage`. Allow values between 30 and 100; clamp out-of-range input. Debounce the change by 1.5s, then reset to page 1 with the new slice size.

30. Changing the page or per-page count clears the current selection (selected tickets are not preserved across pages).

 

**Print / PDF export**

When the user clicks "Print N tickets":

31. For each selected ticket, fetch its comments in parallel via `/api/v2/tickets/{id}/comments.json`. If `metadata.settings.hide_internal_comments` is `true`, filter out comments where `public === false`.

32. Collect all distinct `author_id` values from those comments. Fetch each author via `/api/v2/users/{id}.json` (in parallel). Also include a synthetic system author `{ id: -1, name: 'Zendesk', role: 'admin' }` to attribute system-generated comments.

33. For each selected ticket, fetch audit events via `/api/v2/tickets/{id}/audits.json?include=users,organizations,groups,ticket_forms,sharing_agreements,brands,schedules,satisfaction_reasons&sort_order=desc`. Capture `audits` plus all sideloads (`organizations`, `groups`, `ticket_forms`, `sharing_agreements`, `brands`, `schedules`, `satisfaction_reasons`, `users`) on the ticket — they're needed to render audit events into human-readable text.

34. If "Include custom fields" is on, fetch `/api/v2/ticket_fields.json` once and build a `ticketFieldId → field definition` map so custom fields render with their human labels and option values.

 

**PDF format — must match the original byte-for-byte in layout**

The earlier build produced a generic "ticket card" PDF that did not look like the original — the layout below is what jsPDF on A4 portrait produces and is what users expect to see. Use jsPDF (or any library that lets you place text at exact mm coordinates) and reproduce this layout precisely.

 

35. **Page geometry.** A4 portrait, 210×297mm. Left margin x=20mm, right margin x=190mm (content lives in that 170mm strip). When the y-cursor would exceed 280mm, add a new page and reset y to 20mm. A horizontal full-width line `doc.line(20, y, 190, y)` separates every major section. Body font is jsPDF's default Helvetica.

 

36. **Title row** (y starts at 20). Bold, size 16. Print `#{ticket.id}` at x=20 in blue `rgb(0, 0, 238)` as a hyperlink to the agent URL (transform the API URL by replacing `api/v2` with `agent` and stripping `.json`). Then print the subject at x=55 in black, wrapping at ~48 chars per line. Advance y by ~7mm. Draw a separator line.

 

37. **Header block.** Size 10. Two label rows followed by their value rows:

    - First row of bold labels at: x=20 "Submitted", x=60 "Received via", x=90 "Requester". Below in normal weight: the formatted submitted timestamp ("MMM DD, YYYY HH:mm") at x=20, `via.channel` at x=60, and `{requester} <{requesterEmail}>` at x=90. Use "-" for any missing value.

    - Second row of bold labels at: x=20 "Status", x=40 "Type", x=65 "Priority", x=90 "Group", x=135 "Assignee". Below in normal weight: raw status string, type, priority, group name, and assignee name (wrap assignee at ~30 chars). Use "-" for missing values.

    Sample output (real PDF text):

    ```

    Submitted          Received via    Requester

    Jan 02, 2026 20:49 web             Snowdrop <automationsqa@z3nmail.com>

    Status             Type            Priority      Group       Assignee

    new                incident        low           -           Snowdrop

    ```

    Draw a separator line.

 

38. **Custom ticket fields block** (only when "Include custom fields" was on). Bold, size 12, "Custom ticket fields:" heading at x=20. Then size 10, for each entry in `ticket.custom_fields` whose ID is in the field-definitions map: bold field title at x=20 (wrap at ~60 chars), normal value at x=90 (wrap at ~60 chars; arrays joined by `", "`; null shown as "-"). Advance ~5mm per row (more if either side wrapped). Draw a separator line.

 

39. **Comments block.** Bold, size 12, "Comments:" heading at x=20. Then size 10, for each comment in chronological order:

    - Bold author name at x=20.

    - Same line, normal weight, timestamp at x=130 formatted "MMM DD, YYYY HH:mm".

    - If the comment is private (`!comment.public`), bold grey "Internal note" label at x=166 on the same line.

    - Next lines: comment body, sanitised (see step 42), word-wrapped at ~100 chars.

    - If the comment has attachments: next line, bold "Attachments" at x=20, then each attachment's `file_name` as a blue hyperlink `rgb(0, 50, 165)` to its `content_url`, one per line.

    - Between consecutive comments, draw a horizontal dashed line.

    Draw a separator line at the end.

 

40. **Ticket events block.** Bold, size 12, "Ticket events:" heading at x=20. Then size 10, for each audit in reverse-chronological order:

    - Bold actor name at x=20, normal grey timestamp `rgb(127, 127, 127)` at x=130 ("MMM DD, YYYY HH:mm").

    - Below, font size 9. Render the audit's events with channel-specific phrasing (full string list at the end of this section).

    - Below the event content, in 8pt grey, print any audit `system` metadata that exists, one line each at x=20: `message_id`, `client`, `ip_address`, `location`. Sample tail of an audit:

      ```

      Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 ...

      127.0.0.6

      Singapore, Singapore

      ```

    - Between consecutive audits, draw a horizontal dashed line.

    Draw a final separator line.

 

41. **Field-change events** (the most common audit event) render as a three-column row:

    - Bold field label at x=20 (wrap at ~25 chars).

    - Normal new value at x=75 (wrap at ~35 chars; append `#{id}` if `showId` is set).

    - Normal previous value at x=135 (wrap at ~35 chars) with a horizontal **strike-through** line drawn over each previous-value line so it visibly reads as "was X, now Y".

    Sample output:

    ```

    Status           Open            Pending

    Tags             layout sample_test    sample_test unofffered

                     unofffered

    Priority         Low

    ```

    The previous-value column always has a strikethrough drawn over its text.

 

42. **Sanitise all text** before passing to jsPDF — replace these with ASCII equivalents (jsPDF cannot render them): curly single/double quotes `' ' " "`, em/en dashes `— –`, full-width colon `:`, star `★`, backslash `\`, and `&amp;` (becomes `&`).

 

43. **Colours and emphasis used throughout the PDF:**

    - Greys `rgb(127, 127, 127)` for subdued text (audit timestamps, "Internal note" tag, system metadata, via labels).

    - Hyperlinks: `rgb(0, 50, 165)` for attachment/article links; `rgb(0, 0, 238)` for the title `#{id}` link.

    - Bold for headings, field labels, and authors; normal for everything else.

 

44. **Channel-specific event phrasing** (use these strings exactly — they come from the original app's i18n bundle):

    - **Comment events** — print body text (or grouped comment value) wrapped at ~100 chars. If private and `hide_internal_comments` is false, also show "Internal note" indicator at x=166.

    - **Facebook events** — body text, then for posts show the attachment name as a hyperlink; for messages show share links ("See sticker in Facebook" / "See shared link") and attachment links ("See attachment in Facebook") as clickable links to `external_id` URLs.

    - **Knowledge events** (`KnowledgeLinked` / `KnowledgeCaptured` / `KnowledgeFlagged`) — `{author} inserted a link to "{article}" into this ticket`, `{author} created a new article "{article}" using template "{template}"`, or `{author} flagged "{article}" from this ticket`. For `KnowledgeCaptured` also print bold "Article's URL" + clickable URL, and bold "Template's URL" + clickable URL.

    - **Knowledge resolution** (`KnowledgeLinkAccepted` / `KnowledgeLinkRejected`) — `{requester} found a solution in the following article and marked the ticket as Solved.` then a clickable article title (accepted), or `{requester} rejected "{article}"` with bold "Article's URL" + clickable URL (rejected).

    - **AnswerBot events** (`AutomaticAnswerSend` / `AutomaticAnswerSolve` / `AutomaticAnswerReject` / `AutomaticAnswerViewed`) — render with: `Answer Bot suggested these articles:` (each suggestion as a clickable title plus the end-user review label like "Viewed" / "Not helpful"), `{requester} found a solution …`, `This article has been marked as off-topic: "{article}"` / `{agent} marked the following article as off-topic: "{article}"` / `{agent} removed the off-topic marker for the following article: "{article}"`, and `{requester} viewed "{article}"` — all with clickable article URLs.

    - **Side conversation events** (`CollabThreadCreated` / `CollabThreadReply` / `CollabThreadReopened` / `CollabThreadClosed`) — `Started a new side conversation:` + clickable subject pointing to `{origin}/agent/tickets/{ticketId}/conversations/{threadId}`, `"{subject}" received a new reply` (linked), `Reopened "{subject}"` (linked), `Closed {subject}` (plain).

    - **Via / source line** — for events with a top-level `via` (trigger, automation, macro, ticket merge, follow-up, SLA policy, admin setting, web service, ticket sharing, phone call, voicemail, by email to), print the `via` label/text in grey at x=20 (clickable if it has a link). Strings: `Trigger "{rule_title}"`, `Automation "{rule_title}"`, `Macro "{rule_title}"`, `Merge into ticket #{ticket_id} "{ticket_title}"`, `Follow-up to #{ticket_id} "{ticket_title}"`, `Linked problem #{ticket_id} "{ticket_title}"`, `SLA policy "{sla_policy}"`, `Admin setting "{setting_title}"`, `Via API`, `Via ticket sharing`, `Via incoming phone call`, `Via outgoing phone call`, `Via voicemail`, `By email to {user}`.

    - **Notification / CC / external / org-activity / macro-reference / agent-macro-reference / comment-privacy-change / attachment-redaction / comment-redaction / schedule-assignment / X Corp action / Skills / contextual-workspace / channel-back-failed / SMS / error / chat lifecycle / facebook event** — render each with their dedicated short label string (e.g. "Email notification", "CC notification", "Macro applied", "Comment redaction", "Redacted comment #{comment_id}", "Skills", `Added to {workspace_name} workspace`, `Moved from {old_workspace_name} to {new_workspace_name} workspace`, `Removed from {workspace_name}`, "Text message", "Error", "Message pushed to target", `Chat started:`, `{user} joined the chat`, `{user} left the chat`, `Chat ended:`, `Commented on Facebook Timeline {pageLink}`, `Replied to Facebook Page {pageLink} as private message`).

    - For event types the original app does not parse, append ` (not implemented)` to the event line.

    - For "field changed but field not found": print `Unable to load field label of {field_id} - it may have been deactivated or deleted.` (this exact string appears frequently in real-world PDFs when ticket field IDs no longer exist).

 

45. **Filenames.**

    - Per-PDF: `{requested-date} - {ticket-id}.pdf` where requested-date is "YYYY.MM.DD" (e.g. `2026.03.05 - 93.pdf`).

    - When exactly one ticket is selected, trigger a browser download of that single PDF blob.

    - When more than one is selected: sort tickets by requested date descending, generate every PDF, bundle them into a ZIP, and download. ZIP filename: `{oldest-requested-date} - {newest-requested-date} - {count}.zip` (e.g. `2026.01.02 - 2026.03.05 - 28.zip`).

 

**Notifications**

46. Use `zafClient.invoke('notify', message, type)` for all user-facing errors:

    - Empty search input → `('A name or an email address is required to perform the search.', 'alert')`

    - Search API failure → `('We could not complete your search query. Try again in a moment.', 'alert')`

    - PDF generation failure → `('Failed to create PDF for selected tickets. Try again in a moment.', 'alert')`

    - More than 30 selected → `('You can only select up to 30 tickets to export.', 'error')`

 

**Visual styling**

47. Use the Garden default theme. Primary action colour is Garden blue (Search and Print buttons). Status circles are saturated colour fill with white letter inside, ~22px diameter. Subject popover is a white card with a light-grey border, rounded corners, ~12px padding, and a soft drop shadow. Outer container: ~16px padding. Status/action bar: ~12–16px vertical padding with a subtle bottom border. Search bar: input fills all available width, button on the right. Table: roomy rows (48–56px tall), 12–16px horizontal and 12–14px vertical cell padding, 14px body text, 13px/600-weight headers. The table must not feel dense — if it does, increase padding until each row clearly reads as its own block.

 

**Inline limitations to note**

- The original app uses Pendo/Segment analytics (`window.analytics`). App Builder does not include analytics — omit entirely.

- The original supports RTL via `i18n.isRtl()` and an `@zendesk/client-i18n-tools` integration. App Builder handles localisation through its own i18n layer; let it use defaults.

- The original is hard-capped at 500 search results (5 pages of the search API) and 30 selected tickets per export — keep both caps; they are intentional safeguards against runaway PDF jobs.

- The original's `groups.json` call fetches all groups in a single page. If your account has more than 100 groups, you may need to paginate — App Builder can handle this if it surfaces during testing.

 

**Acceptance checklist (verify these before declaring the app done)**

- [ ] `manifest.json` parameter renders as a checkbox on the install screen, not a text input.

- [ ] The search input visibly fills the entire width of the search bar (no fixed-width gap before the Search button).

- [ ] Table rows feel roomy — every row clearly stands apart with ~48–56px of height.

- [ ] First column of every row is a Garden `Checkbox` (not a row-level click handler). Header has its own select-all `Checkbox` with `indeterminate` state.

- [ ] Every checkbox uses `<Label style={{ color: 'transparent' }}>.</Label>` — the dot is present in markup but invisible against any background; no readable text appears next to any checkbox.

- [ ] Pagination shows numbered page buttons followed by a `>` chevron (e.g. `1 2 3 >`). Clicking page `2` advances to tickets `31–60`.

- [ ] Downloaded PDF matches the sample format: `#id` blue link in title, two-row header block ("Submitted/Received via/Requester" then "Status/Type/Priority/Group/Assignee"), "Comments:" section with author + "MMM DD, YYYY HH:mm" timestamps, "Ticket events:" section with field-change rows showing strikethrough on previous values, and a system-metadata footer line in 8pt grey for each audit.

 

Enhancement Options

Based on your organizational needs, you can extend the Print Ticket History app to support your specific needs using App Builder.

 

Important Note: This app recipe is for the Print Ticket History app. Any modifications to the recipe or its underlying logic may alter the core functionality of the app. Thoroughly test all changes in a non-production environment before deploying to your production workspace.

Please note: that as App Builder evolves including updates to underlying AI models, design components, and other dependencies, we cannot guarantee that this recipe will continue to function as expected over time. We recommend periodically validating the recipe against any platform updates.