Skip to content

Commit 4550c4f

Browse files
rgarciadprevoznik
andauthored
Document named Playwright executors (#681)
* Document named Playwright executors * Note sync import and define busy in executors docs --------- Co-authored-by: rgarcia <72655+rgarcia@users.noreply.github.com> Co-authored-by: Daniel Prevoznik <danny@onkernel.com>
1 parent 03a8fff commit 4550c4f

4 files changed

Lines changed: 214 additions & 3 deletions

File tree

‎browsers/playwright-execution.mdx‎

Lines changed: 202 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ When you execute Playwright code through this API:
1414
- You have access to `page`, `context`, `browser`, and browser-wide `webmcp` helpers
1515
- You can `return` a value, which is returned in the response
1616
- Execution is isolated in a fresh context each time
17+
- Every call runs in an [executor](#executors). Calls without `executor` share the default executor and the active tab; named executors each own a tab and run concurrently with each other
1718

1819
## Quick example
1920

@@ -104,7 +105,7 @@ kernel browsers playwright execute <session_id> 'await page.goto("https://www.on
104105

105106
Your code has access to these objects:
106107

107-
- `page` - The current page instance
108+
- `page` - The page the executor is bound to: the active tab in the default executor, or the named executor's own tab (see [Executors](#executors))
108109
- `context` - The browser context
109110
- `browser` - The browser instance
110111
- `webmcp` - Helper for discovering and invoking [WebMCP tools](/browsers/webmcp)
@@ -223,6 +224,206 @@ fmt.Println(response.Result) // map[title:Example Domain url:https://example.com
223224
```
224225
</CodeGroup>
225226

227+
## Executors
228+
229+
Every call runs in an executor: a dedicated Node.js process in the browser's VM with its own connection to Chromium. Calls on the same executor run one at a time, in the order they arrive. Calls on different executors run concurrently, and a timeout, crash, or blocked event loop in one executor doesn't affect the others.
230+
231+
Use named executors to drive several tabs of one browser in parallel. Give each independent task its own executor name, and reuse a name for the sequential steps of one task.
232+
233+
### The default executor
234+
235+
Calls without `executor` run in the executor named `default`, which always exists. Passing `executor: "default"` is the same as omitting it. In the default executor, `page` is bound to an active tab reported by Chrome (the foreground tab in single-window sessions). Use `browser.contexts()` to select a context or page explicitly.
236+
237+
### Named executors
238+
239+
Pass any other name (`^[A-Za-z0-9_-]{1,64}$`) to run the call in a named executor. The first call with a new name creates it. Each named executor owns a tab: its first call opens a new background tab in the default browser context, and `page` is bound to that tab on every later call while it stays open. Opening it doesn't change the active tab of an existing window. If the tab is closed, the next call opens a new one.
240+
241+
Ownership only decides what `page` is bound to. Executor code can still reach other tabs through `context` and `browser`.
242+
243+
The response includes a `tab` object with the tab `page` was bound to:
244+
245+
```json
246+
{
247+
"success": true,
248+
"result": "Example Domain",
249+
"tab": { "target_id": "8F2C1D4E9A7B3C6D5E1F2A3B4C5D6E7F", "created": true }
250+
}
251+
```
252+
253+
`created` is `true` when this call opened the tab. `tab` is absent if the call failed before binding a tab.
254+
255+
### Run tasks in parallel
256+
257+
This example runs two tasks at the same time in two named executors, reuses one of them, then lists and deletes an executor. It uses an existing session and client, as in the examples above.
258+
259+
<CodeGroup>
260+
```typescript Typescript/Javascript
261+
const task = (executor: string, url: string) =>
262+
kernel.browsers.playwright.execute(sessionId, {
263+
executor,
264+
code: `
265+
await page.goto(${JSON.stringify(url)});
266+
return { title: await page.title(), url: page.url() };
267+
`,
268+
timeout_sec: 10,
269+
});
270+
271+
// Each task opens its own background tab; both run concurrently
272+
const [docs, home] = await Promise.all([
273+
task('docs', 'https://www.kernel.sh/docs/'),
274+
task('home', 'https://www.kernel.sh/'),
275+
]);
276+
console.log(docs.result, docs.tab); // { title: ..., url: ... } { target_id: '...', created: true }
277+
278+
// A later call on the same executor reuses its tab
279+
const again = await kernel.browsers.playwright.execute(sessionId, {
280+
executor: 'docs',
281+
code: 'return page.url();',
282+
});
283+
console.log(again.tab?.created); // false
284+
285+
// Inspect and clean up; the default executor is always listed first
286+
const { executors } = await kernel.browsers.playwright.executors.list(sessionId);
287+
console.log(executors.map(({ name, busy, url }) => ({ name, busy, url })));
288+
289+
await kernel.browsers.playwright.executors.delete('home', {
290+
id_or_name: sessionId,
291+
close_tab: true,
292+
});
293+
```
294+
295+
```python Python
296+
import asyncio
297+
298+
from kernel import AsyncKernel
299+
300+
kernel = AsyncKernel()
301+
302+
303+
async def task(executor: str, url: str):
304+
return await kernel.browsers.playwright.execute(
305+
session_id,
306+
executor=executor,
307+
code=f"""
308+
await page.goto({url!r});
309+
return {{ title: await page.title(), url: page.url() }};
310+
""",
311+
timeout_sec=10,
312+
)
313+
314+
315+
async def main():
316+
# Each task opens its own background tab; both run concurrently
317+
docs, home = await asyncio.gather(
318+
task("docs", "https://www.kernel.sh/docs/"),
319+
task("home", "https://www.kernel.sh/"),
320+
)
321+
print(docs.result, docs.tab.created) # {'title': ..., 'url': ...} True
322+
323+
# A later call on the same executor reuses its tab
324+
again = await kernel.browsers.playwright.execute(
325+
session_id,
326+
executor="docs",
327+
code="return page.url();",
328+
)
329+
print(again.tab.created) # False
330+
331+
# Inspect and clean up; the default executor is always listed first
332+
listing = await kernel.browsers.playwright.executors.list(session_id)
333+
print([(e.name, e.busy, e.url) for e in listing.executors])
334+
335+
await kernel.browsers.playwright.executors.delete(
336+
"home",
337+
id_or_name=session_id,
338+
close_tab=True,
339+
)
340+
341+
342+
asyncio.run(main())
343+
```
344+
345+
```go Go
346+
// Add "sync" to the imports from the example above
347+
task := func(executor, url string) (*kernel.BrowserPlaywrightExecuteResponse, error) {
348+
return client.Browsers.Playwright.Execute(ctx, sessionID, kernel.BrowserPlaywrightExecuteParams{
349+
Executor: kernel.String(executor),
350+
Code: fmt.Sprintf(`
351+
await page.goto(%q);
352+
return { title: await page.title(), url: page.url() };
353+
`, url),
354+
TimeoutSec: kernel.Int(10),
355+
})
356+
}
357+
358+
// Each task opens its own background tab; both run concurrently
359+
var wg sync.WaitGroup
360+
results := make([]*kernel.BrowserPlaywrightExecuteResponse, 2)
361+
for i, t := range []struct{ executor, url string }{
362+
{"docs", "https://www.kernel.sh/docs/"},
363+
{"home", "https://www.kernel.sh/"},
364+
} {
365+
wg.Add(1)
366+
go func() {
367+
defer wg.Done()
368+
response, err := task(t.executor, t.url)
369+
if err != nil {
370+
panic(err)
371+
}
372+
results[i] = response
373+
}()
374+
}
375+
wg.Wait()
376+
fmt.Println(results[0].Result, results[0].Tab.TargetID, results[0].Tab.Created)
377+
378+
// A later call on the same executor reuses its tab
379+
again, err := client.Browsers.Playwright.Execute(ctx, sessionID, kernel.BrowserPlaywrightExecuteParams{
380+
Executor: kernel.String("docs"),
381+
Code: `return page.url();`,
382+
})
383+
if err != nil {
384+
panic(err)
385+
}
386+
fmt.Println(again.Tab.Created) // false
387+
388+
// Inspect and clean up; the default executor is always listed first
389+
listing, err := client.Browsers.Playwright.Executors.List(ctx, sessionID)
390+
if err != nil {
391+
panic(err)
392+
}
393+
for _, e := range listing.Executors {
394+
fmt.Println(e.Name, e.Busy, e.URL)
395+
}
396+
397+
err = client.Browsers.Playwright.Executors.Delete(ctx, "home", kernel.BrowserPlaywrightExecutorDeleteParams{
398+
IDOrName: sessionID,
399+
CloseTab: kernel.Bool(true),
400+
})
401+
if err != nil {
402+
panic(err)
403+
}
404+
```
405+
</CodeGroup>
406+
407+
### Limits
408+
409+
A browser can have at most **8 named executors**; the default executor doesn't count. A call that would create a ninth returns `409` with a `message` and an `executors` array listing the current executors, so you can delete one and retry.
410+
411+
Named executors aren't removed automatically while the browser runs. They're removed and their tabs closed when the browser shuts down. Delete executors you're done with so long-lived browsers don't hit the limit.
412+
413+
A call with `executor` to a browser whose image predates executors fails with `400`. Calls without `executor` work on every image.
414+
415+
### List and delete executors
416+
417+
`GET /browsers/{id_or_name}/playwright/executors` returns the browser's executors, default first. Each entry has `name`, `busy` (whether a call is currently running on the executor), `created_at`, and `last_used_at`; named executors with an open tab also report `target_id` and `url`.
418+
419+
`DELETE /browsers/{id_or_name}/playwright/executors/{name}` stops the executor's process and returns `204`. It closes the executor's tab unless you pass `close_tab=false`. A call running on the executor fails with an error saying the executor was deleted, and the name can be reused afterwards. Unknown names return `404`.
420+
421+
Deleting `default` restarts it instead of removing it: its process is stopped, and queued and later calls run on a new process. It owns no tab, so `close_tab` has no effect. Use this to recover the default executor from a stuck state.
422+
423+
### Timeouts and crashes
424+
425+
After a timeout, the executor keeps its process but drops its browser connection, so code abandoned by the timeout can't keep driving the browser. After a crash or a blocked event loop, the next call on that executor starts a fresh process. Either way, other executors keep running.
426+
226427
## Timeout configuration
227428

228429
Set `timeout_sec` for the work each request performs. The API defaults to 60 seconds and allows up to 300 seconds, but most short scripts don't need that budget. Start with:

‎browsers/profiles/concurrency.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ A browser loads profile data as a snapshot. Saving replaces the profile's comple
1010

1111
For a personal assistant or another workflow where concurrent tasks act as the same end user, start one browser with that user's profile and open multiple tabs in it. Tabs in the same browser context share a live cookie jar and persistent origin storage, so a login or cookie update in one tab is available to the others without loading the profile again or restarting Chrome. Tab-local state such as `sessionStorage` remains separate.
1212

13-
Open additional tabs with Playwright's `context.newPage()`. Keep each task on its own `Page`, and coordinate actions that change shared account or browser state. See [Playwright Execution](/browsers/playwright-execution) for ways to run code against the browser.
13+
Give each task its own named [Playwright executor](/browsers/playwright-execution#executors). Each executor owns a tab and runs concurrently with the others, so `page` in a task's calls is always that task's tab. Coordinate actions that change shared account or browser state. See [Playwright Execution](/browsers/playwright-execution) for ways to run code against the browser.
1414

1515
<Tip>
1616
Headful, non-GPU browsers use `8GiB` by default. For tab-heavy workloads, set `memory` to `16GiB` when you [create the browser](/api-reference/browsers/create-a-browser-session).

‎browsers/repl.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ The API process directly owns one lazily started Node child and is its sole supe
359359
| Execution timeout | Child process group destroyed | Replaced on next request |
360360
| Crash, OOM, uncaught asynchronous exception, or protocol corruption | Child process group destroyed | Replaced on next request |
361361

362-
Timeouts are destructive because abandoned JavaScript cannot safely coexist with a later cell. Check `response.repl_terminated` to see whether your own request destroyed the REPL it ran in — the next call starts a fresh one and earlier top-level bindings are gone. Calls are serialized, so executions on the same browser cannot interleave.
362+
Timeouts are destructive because abandoned JavaScript cannot safely coexist with a later cell. Check `response.repl_terminated` to see whether your own request destroyed the REPL it ran in — the next call starts a fresh one and earlier top-level bindings are gone. Calls are serialized, so executions on the same browser cannot interleave. To run Playwright tasks in parallel across tabs of one browser, use named [executors](/browsers/playwright-execution#executors) with Playwright execution.
363363

364364
## Error handling
365365

‎changelog.mdx‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@ import { YouTubeVideo } from '/snippets/youtube-video.mdx';
99
For API library updates, see the [Node SDK](https://github.com/onkernel/kernel-node-sdk/blob/main/CHANGELOG.md), [Python SDK](https://github.com/onkernel/kernel-python-sdk/blob/next/CHANGELOG.md), and [Go SDK](https://github.com/onkernel/kernel-go-sdk/blob/main/CHANGELOG.md) changelogs.
1010
</Note>
1111

12+
<Update label="October 6" tags={["Product", "Docs"]}>
13+
## Product updates
14+
15+
- Added **named [Playwright executors](/browsers/playwright-execution#executors)**. Pass `executor` to `playwright.execute` to run the call in a dedicated process that owns its own tab. Calls on different executors run concurrently and fail independently; calls on the same executor run one at a time. Calls without `executor` keep running in the `default` executor on the active tab. The response now includes the `tab` the call was bound to, a browser can have up to 8 named executors, and new endpoints list and delete them. Available in the Node, Python, and Go SDKs from 0.120.0.
16+
17+
## Documentation updates
18+
19+
- Documented [executors](/browsers/playwright-execution#executors) in the Playwright execution guide, with a multi-language example that runs two tasks in parallel in one browser. Updated the [profile concurrency](/browsers/profiles/concurrency) and [Browser REPL](/browsers/repl) pages to point at named executors for parallel work across tabs.
20+
</Update>
21+
1222
<Update label="October 2" tags={["Product", "Docs"]}>
1323
## Product updates
1424

0 commit comments

Comments
 (0)