The Screenplay Pattern
Actors, abilities, tasks and questions — a compositional alternative to page objects for suites with many user types and deep flows.
2 min read · updated 19 September 2026
The page object model organises automation around screens. The screenplay pattern organises it around actors performing tasks, which composes better when there are several kinds of user and the flows are deep.
// Java, Serenity Screenplay
alice.attemptsTo(
SignIn.withCredentials(alice.usernameAndPassword()),
AddToBasket.theProduct("Field Notes").times(2),
CheckOut.payingWith(VISA_TEST_CARD)
);
alice.should(seeThat(TheOrder.status(), is("confirmed")));Read it aloud and it is a user story. That is the design goal.
#The four pieces
Actor — who is doing it. Carries identity and state, which is what makes multi-user scenarios natural.
Ability — what the actor can do: browse the web, call an API, query the database. Abilities are how an actor gets a browser, and how the same task can be performed through different interfaces.
Task — a thing the user wants, composed of other tasks and interactions.
CheckOut is a task; Click.on(PAY_BUTTON) is an interaction.
Question — something the actor can observe. TheOrder.status() returns a
value the test asserts on.
// TypeScript, hand-rolled. The whole pattern is four small interfaces.
interface Ability { }
class BrowseTheWeb implements Ability {
constructor(readonly page: Page) {}
static as(actor: Actor): BrowseTheWeb { return actor.abilityTo(BrowseTheWeb); }
}
interface Task { performAs(actor: Actor): Promise<void>; }
interface Question<T> { answeredBy(actor: Actor): Promise<T>; }
class Actor {
private abilities = new Map<Function, Ability>();
constructor(readonly name: string) {}
whoCan(...abilities: Ability[]): this {
for (const ability of abilities) this.abilities.set(ability.constructor, ability);
return this;
}
abilityTo<A extends Ability>(type: new (...args: never[]) => A): A {
const ability = this.abilities.get(type);
if (!ability) throw new Error(`${this.name} cannot ${type.name}`);
return ability as A;
}
async attemptsTo(...tasks: Task[]) {
for (const task of tasks) await task.performAs(this);
}
async asks<T>(question: Question<T>): Promise<T> {
return question.answeredBy(this);
}
}// A task: composed, reusable, and named after the user's intent.
class AddToBasket implements Task {
private constructor(private readonly product: string, private readonly quantity: number) {}
static theProduct(product: string) {
return { times: (quantity: number) => new AddToBasket(product, quantity) };
}
async performAs(actor: Actor) {
const { page } = BrowseTheWeb.as(actor);
await page.goto(`/products/${slugify(this.product)}`);
await page.getByLabel('Quantity').fill(String(this.quantity));
await page.getByRole('button', { name: 'Add to basket' }).click();
}
}
// A question: observation, not assertion. The test decides what is expected.
const BasketCount: Question<number> = {
async answeredBy(actor) {
const { page } = BrowseTheWeb.as(actor);
return Number(await page.getByTestId('basket-count').innerText());
}
};#What it buys over page objects
Multiple actors in one test, naturally. This is the biggest single advantage, and it is the reason to reach for the pattern at all.
const alice = new Actor('Alice').whoCan(new BrowseTheWeb(alicePage));
const bob = new Actor('Bob').whoCan(new BrowseTheWeb(bobPage));
await alice.attemptsTo(ShareDocument.with(bob).asCommenter());
await bob.attemptsTo(OpenTheDocument.sharedByAlice());
expect(await bob.asks(CanEdit.theDocument())).toBe(false); // authorization, testedExpressing that with page objects means two driver instances threaded through helper functions by hand. Here it falls out of the model. See authorization testing — this is the cheapest way to write those tests.
Tasks compose. CheckOut is used by twenty tests and built from
EnterCard, ConfirmAddress, Submit. Change checkout once.
The same task, a different interface. An actor whoCan(CallAnApi)
rather than BrowseTheWeb can perform AddToBasket through HTTP. Setup
becomes fast without a second vocabulary — the point made in
end-to-end testing about arranging via
the API.
Reports that read like prose. Serenity generates documentation from the task names, which is the strongest form of living documentation going.
#What it costs
More indirection. A one-line click becomes an interaction class. For a small suite this is pure overhead, and page objects are the right answer.
More concepts for a new joiner. Actor, ability, task, interaction and question are five ideas before anyone writes a test.
Tooling support is thinner outside the JVM. Serenity is excellent;
@serenity-js covers Node well; elsewhere you are hand-rolling, which is
fine but is a decision to make deliberately.
#When to choose it
Reach for screenplay when at least two of these are true:
- More than two distinct user roles whose permissions differ
- Flows that reuse the same steps across many tests
- Tests that need two users interacting
- A UI that is not a set of pages
- A suite large enough that the indirection cost is amortised
Otherwise use page objects, which are less code and perfectly adequate for most suites.
Common questions
- Is the screenplay pattern worth the extra machinery?
- For a suite of thirty tests with one user type, no — page objects are less code and just as clear. It starts paying somewhere around several user roles, tasks reused across many flows, or a UI that is not organised into pages.
- Do I need Serenity BDD to use screenplay?
- No. Serenity is the best-known implementation and gives you reporting for free, but the pattern is four small interfaces and can be hand-rolled in any language in an afternoon.
- How does screenplay relate to the page object model?
- It replaces the page as the unit of abstraction with the task. Page objects still exist underneath, usually as thin locator holders; what changes is that tests compose tasks performed by actors rather than calling methods on screens.
Runnable samples for this page
last test results ↗- TypeScript
typescript/src/practices/screenplay-pattern
Working tests, not fragments — they run in CI on every push to 8exgh/endtoendtester-samples.
Was this page useful?
Related topics
- The Page Object ModelEncapsulating a screen behind a class so tests describe intent rather than selectors — what it fixes, the god-object failure mode, and modern alternatives.
- End-to-End TestingWhat belongs in an end-to-end suite and what does not, how many journeys are enough, and the practices that keep a browser suite from becoming the thing everyone ignores.
- Behaviour-Driven DevelopmentBDD as a conversation practice rather than a tool choice — what the three amigos session produces, when Cucumber earns its place, and how it fails.
- Authorization TestingThe highest-value security testing most teams are not doing — proving that the user who should not be able to do a thing genuinely cannot.