PRACTICAL GUIDE / playwright tags and annotations

Operational Test Metadata with Playwright Tags, Annotations, and TestInfo

Use Playwright tags, annotations, TestInfo, and attachments to route suites, record runtime context, preserve artifacts, and improve reporter output.

By The Testing AcademyUpdated July 11, 20269 min read
All field guides
In this guide13 sections
  1. Define a Metadata Contract Before Adding Labels
  2. Use Tags for Selection
  3. Make Grep Expressions Reviewable
  4. Use Annotations for Explanatory Context
  5. Add Runtime Annotations Only When Context Emerges
  6. Use TestInfo as the Artifact Boundary
  7. Capture Failure Evidence with an Automatic Fixture
  8. Use Built-In Status Semantics Correctly
  9. Design Reporter Consumption Deliberately
  10. Diagnose Metadata Failures
  11. Balance Rich Context Against Noise
  12. Operational Checklist
  13. Conclusion: Treat Metadata as a Shared Interface

What you will learn

  • Define a Metadata Contract Before Adding Labels
  • Use Tags for Selection
  • Make Grep Expressions Reviewable
  • Use Annotations for Explanatory Context

Test metadata is operational infrastructure. It decides which Playwright tests run in a pull request, who receives a failed build, which requirement a result supports, and what evidence survives in the report. Tags, annotations, and attachments only help when their meanings are stable enough for people and automation to trust.

A useful metadata contract separates selection from explanation. Tags provide a small filter vocabulary. Annotations preserve typed context. TestInfo contributes runtime identity and safe artifact paths. Reporters consume those values; they should not have to reverse-engineer meaning from decorative title text.

Define a Metadata Contract Before Adding Labels

Start with the questions your pipeline must answer. Typical dimensions are product area, test tier, risk class, and owner. Keep each vocabulary short: @checkout, @smoke, @p0, and @team-payments are understandable if their definitions live in one contributor document and CI uses the same tokens.

Do not tag implementation details that can be read from the project, file path, or browser name. Avoid synonyms such as @critical, @priority-zero, and @p0 for one concept. Every extra spelling weakens filters and dashboards.

The official Playwright annotations guide requires tags to begin with @ and supports tags and annotations in test or group details. That declarative form is easier for discovery and reporting than mutating metadata late without need.

Animated field map

Operational Metadata Through Playwright

Ownership and risk metadata select execution, gain runtime context, collect evidence, and reach the final reporter.

  1. 01 / ownership metadata

    Ownership Metadata

    Declare product area, risk, owner, and requirement using governed values.

  2. 02 / tag filtering

    Tag Filtering

    Select an intentional test subset with reviewed grep expressions.

  3. 03 / runtime annotation

    Runtime Annotation

    Record context that becomes known only during fixture or test execution.

  4. 04 / artifact attachment

    Artifact Attachment

    Preserve bounded, sanitized diagnostics under a unique test output path.

  5. 05 / reporter output

    Reporter Output

    Expose routing context and evidence to humans and CI integrations.

Use Tags for Selection

Declare tags in the details object so they remain separate from the human title. A test can have several orthogonal tags, but each should answer a real filtering question. Group-level tags are useful when every child test belongs to the same area.

TypeScript
import { expect, test } from "@playwright/test";

test.describe("refunds", { tag: ["@payments", "@refunds"] }, () => {
  test("an operator can approve a pending refund", {
    tag: ["@smoke", "@p0"],
  }, async ({ page }) => {
    await page.goto("/refunds/RF-1042");
    await page.getByRole("button", { name: "Approve refund" }).click();
    await expect(page.getByRole("status")).toHaveText("Refund approved");
  });
});

The child receives useful suite metadata without repeating it in every title. Keep the business title readable when tags are rendered beside it in an HTML report.

Make Grep Expressions Reviewable

--grep matches the test's searchable title and tags. Simple single-tag filters are easy to reason about. For OR and AND combinations, quote the regular expression in shell commands and keep the CI expression visible in configuration rather than constructing it dynamically from undocumented variables.

Shell
npx playwright test --grep "@smoke"
npx playwright test --grep "@smoke|@p0"
npx playwright test --grep "(?=.*@payments)(?=.*@smoke)"
npx playwright test --grep-invert "@manual-only"

Run --list with production filters during CI configuration changes and inspect the selected tests. A filter that silently selects zero tests is a pipeline defect. A broad token embedded in ordinary title text can also match unexpectedly, which is another reason to keep tag names distinctive.

Use Annotations for Explanatory Context

Annotations have a type and a description. They are appropriate for an issue URL, requirement id, owner, or a reason that should appear in reporter output but should not select execution. Use a small type taxonomy and structured descriptions that can be parsed if an integration needs them.

TypeScript
test("declined payment leaves the order unpaid", {
  tag: ["@payments", "@regression"],
  annotation: [
    { type: "owner", description: "team-payments" },
    { type: "requirement", description: "PAY-DECLINE-03" },
    { type: "risk", description: "revenue-protection" },
  ],
}, async ({ page }) => {
  await page.goto("/checkout");
  // Enter a deterministic declined-payment scenario and assert the order state.
});

Do not put a changing build URL or raw request payload into a declaration. Static annotations should remain valid every time the test is discovered.

Add Runtime Annotations Only When Context Emerges

Some values are unknown until a fixture leases an account or the server assigns a cohort. Add those through test.info().annotations once the value exists. Keep descriptions bounded and safe for report publication.

TypeScript
import { test as base } from "@playwright/test";

type TenantFixtures = {
  tenant: { id: string; class: "standard" | "enterprise" };
};

export const test = base.extend<TenantFixtures>({
  tenant: async ({ request }, use) => {
    const tenant = await leaseTenant(request);
    test.info().annotations.push({
      type: "tenant-class",
      description: tenant.class,
    });

    await use(tenant);
    await releaseTenant(request, tenant.id);
  },
});

The class is useful for triage; the tenant's secret token is not. If a reporter must aggregate runtime annotations, confirm that it reads the final test result rather than only discovery-time metadata.

Use TestInfo as the Artifact Boundary

TestInfo identifies the running test, project, retry, output directory, expected status, and actual status. Prefer those fields to reconstructing identity from filenames or environment variables. testInfo.outputPath(...) gives each test a non-conflicting location even under parallel execution.

testInfo.attach accepts a body or a path, not both. Await the call because Playwright copies path-based attachments into reporter-accessible storage. Choose a meaningful content type so reports can render or download the evidence correctly.

TypeScript
import fs from "node:fs/promises";
import { test } from "@playwright/test";

test("export includes the requested date range", async ({ page }, testInfo) => {
  await page.goto("/reports");
  await page.getByLabel("From").fill("2026-07-01");
  await page.getByLabel("To").fill("2026-07-10");

  const context = {
    project: testInfo.project.name,
    retry: testInfo.retry,
    dateRange: { from: "2026-07-01", to: "2026-07-10" },
  };

  const contextFile = testInfo.outputPath("report-context.json");
  await fs.writeFile(contextFile, JSON.stringify(context, null, 2), "utf8");
  await testInfo.attach("report-context", {
    path: contextFile,
    contentType: "application/json",
  });
});

The temporary file is local to the test result. Avoid writing artifacts to a shared fixed path where workers can overwrite one another.

Capture Failure Evidence with an Automatic Fixture

An automatic fixture can attach diagnostics only when actual and expected status differ. This keeps successful reports lean while standardizing failure context. Capture small summaries first; traces, screenshots, and videos already have dedicated Playwright policies.

TypeScript
import { test as base } from "@playwright/test";

type DiagnosticFixtures = { failureContext: void };

export const test = base.extend<DiagnosticFixtures>({
  failureContext: [async ({ page }, use, testInfo) => {
    await use();

    if (testInfo.status !== testInfo.expectedStatus) {
      const summary = {
        url: page.url(),
        title: await page.title().catch(() => "unavailable"),
        project: testInfo.project.name,
        retry: testInfo.retry,
      };

      await testInfo.attach("failure-context", {
        body: JSON.stringify(summary, null, 2),
        contentType: "application/json",
      });
    }
  }, { auto: true }],
});

If pages can contain personal data in URLs or titles, sanitize before attaching. Diagnostic convenience does not override the report's retention and access policy.

Use Built-In Status Semantics Correctly

skip, fixme, and slow communicate execution behavior, not product taxonomy. Use conditional forms with a reason when a browser or environment genuinely cannot support the scenario. Do not transform a long-lived product defect into a silent skip without an issue annotation and owner.

Retries also belong in configuration or test policy, not a @retry tag that no runner understands. A quarantine tag can select a separate job, but the report must still preserve expected status, issue, and expiry information through real configuration and annotations.

Design Reporter Consumption Deliberately

Built-in reporters display titles, projects, annotations, attachments, steps, and status in different formats. A custom reporter should treat metadata as input, not infer ownership from directory spelling when a declared owner exists. Keep compatibility in mind if several reporters run together, such as terminal output plus HTML and a CI integration.

Version the metadata taxonomy like any shared interface. Before renaming @checkout to @purchase, update CI grep expressions, dashboards, contributor guidance, and reporter mappings in one change. Otherwise the suite and operational tooling disagree about which tests exist.

Diagnose Metadata Failures

An unexpectedly empty CI job usually means a grep expression or renamed tag no longer matches. Duplicate owner annotations indicate inheritance and test-level overrides need a policy. Missing attachments often come from not awaiting attach, deleting a file too early, or writing outside a test-owned output path.

Huge reports point to unconditional attachments or repeated payload dumps. Leaked credentials point to missing sanitization at the attachment boundary. If runtime annotations never reach a dashboard, confirm the reporter reads completed results. If engineers cannot tell whether a token is a tier, area, or status, the taxonomy has outgrown informal naming.

Balance Rich Context Against Noise

More metadata can improve routing while making every test declaration hard to scan. Inherit stable area and owner values at a describe level, and declare case-specific risk or requirements at the test. Runtime values should be added only when they improve reproducibility or aggregation.

Attachments cost storage, transfer time, and review attention. Capture them on failure or for tests whose output is itself under examination. Prefer a concise JSON summary over a full database export, and rely on trace retention policy for detailed browser history.

Define retention and visibility with the same care as capture. A harmless local attachment may enter a long-lived CI artifact or external dashboard, so metadata producers need to know where reporter output is published and who can read it.

Operational Checklist

  • Define a short vocabulary for area, tier, risk, and owner.
  • Prefix every tag with @ and eliminate synonyms.
  • Keep tags for selection and annotations for explanation.
  • Validate CI grep expressions with the discovered test list.
  • Add static metadata declaratively at test or describe scope.
  • Add runtime annotations only after sanitizing their descriptions.
  • Write temporary files through testInfo.outputPath.
  • Await every attachment and set the correct content type.
  • Attach bounded evidence on failure instead of dumping all state.
  • Update reporters and CI filters whenever the taxonomy changes.

Conclusion: Treat Metadata as a Shared Interface

Choose the filters the pipeline actually needs, codify them as a small tag vocabulary, and move explanatory details into typed annotations. Use TestInfo for runtime identity and isolated artifact paths, then publish only sanitized evidence that improves triage. Once CI, reports, and contributors share the same definitions, metadata becomes dependable routing information instead of decoration.

// LIVE COURSE / THE TESTING ACADEMY

Playwright Automation Mastery

Go beyond Selenium. Master Playwright with JS/TS in 90 days.

From the instructor behind this guide.

Playwright jobs are growing 8x faster than Selenium. 90 days / 75+ live hrs / Tue-Thu-Sat 7 AM IST.

Code PROMODE / 10% offJoin the batch

The Testing Academy editorial desk

Practical QA guidance built around test evidence, production tradeoffs, and interview-ready explanations.

Published July 11, 2026 / Reviewed July 11, 2026

PRIMARY REFERENCES

Verify the details at the source

QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.

  1. 01
    Playwright documentation

    Microsoft

    Canonical API, locator, fixture, browser, and test-runner behavior.

  2. 02
    Playwright best practices

    Microsoft

    Official guidance for resilient tests, isolation, and user-facing locators.

  3. 03
    WebDriver standard

    W3C

    The browser automation protocol specification used by major automation stacks.

FAQ / QUICK ANSWERS

Questions testers ask

What is the difference between a Playwright tag and annotation?

A tag is an `@`-prefixed token suited to filtering and grouping tests. An annotation carries a type and optional description, making it better for ownership, issue links, requirements, or runtime context shown by reporters.

How do I run Playwright tests by tag?

Add tags in the test or describe details object, then use `--grep` or `--grep-invert` with an intentional regular expression. Test the filter in CI because multiple tags and title text participate in matching.

Can annotations be added while a test is running?

Yes. Push a type and description into `test.info().annotations` when the value is only known at runtime, such as a selected tenant class or feature-flag cohort.

How should files be attached with TestInfo?

Call `testInfo.attach` with either a body or a file path and await it. Use `testInfo.outputPath` for collision-free temporary files when tests run in parallel, and remove secrets before attaching data.

Should retry and quarantine status be represented only by tags?

No. Tags can select an operational group, but skip, fixme, slow, retries, and issue tracking each have specific semantics. Preserve the reason in an annotation or configuration instead of encoding the whole lifecycle in a tag name.