Developer Preview: This workflow is for building reliable provider plugins. It uses a real managed browser session to discover the requests that later run through browser.sessionFetch.
Goal
Use visible browser work once to learn the provider’s request shape. Then run repeatable skills through authenticated provider APIs.
The default loop is:
open real session -> perform UI action -> capture request -> replay with sessionFetch -> validate shapeThis keeps login, OTP, captcha, and consent screens in the browser while moving carts, checkout previews, account reads, order status, and profile updates into API helpers.
Capture a real session
Start from the same managed browser profile the plugin will use in production. Log in through setup or recover, then perform the provider action once through the UI.
Capture the network request that represents the action. For each candidate request, record:
- method and URL
- GraphQL
operationNameor REST endpoint - minimum request body
- required variables
- response shape
- status codes and provider error payloads
- headers that are stable enough to send again
- whether the request needs the browser session cookies
Do not copy raw cookies, OTPs, tokens, or account secrets into plugin state. The runtime owns the session.
Replay through sessionFetch
Put the captured request behind a provider helper.
export async function getOrderStatus({
browser,
orderRef,
}: {
browser: BrowserProfile;
orderRef: string;
}) {
const response = await browser.sessionFetch('https://api.sample.example/graphql', {
method: 'POST',
json: {
operationName: 'OrderStatusQuery',
variables: { orderRef },
query: ORDER_STATUS_QUERY,
},
});
if (response.status === 429) {
return { ok: false as const, errorCode: 'rate_limited' };
}
if (!response.ok) {
return { ok: false as const, errorCode: 'provider_unavailable' };
}
return {
ok: true as const,
data: OrderStatusResponse.parse(await response.json()),
};
}Pin the operationName and the smallest variable set that works. Keep the provider’s full response out of user-facing logs and plugin state.
Validate drift
Providers change request and response shapes. Treat that as normal.
Use lightweight shape validators in helpers:
import { z } from 'zod';
const OrderStatusResponse = z.object({
data: z.object({
order: z.object({
status: z.string(),
etaMinutes: z.number().optional(),
}),
}),
});When validation fails, return provider_unavailable with retryable: true and log structured fields that help debug the shape change.
log.warn('Order status response changed', {
operationName: 'OrderStatusQuery',
status: response.status,
});Add a canary skill
For high-value providers, add a read-only canary skill that exercises the smallest authenticated request. Run it before mutating flows or on a schedule controlled by the platform.
A good canary:
- requires no user-visible state change
- uses
browser.sessionFetch - validates the response shape
- returns
rate_limitedseparately fromprovider_unavailable - logs the operation name or endpoint in fields
When to fall back to browser.act
Use browser.act when the provider step has no stable request to replay:
- login
- OTP entry
- captcha or risk challenge
- consent or terms screen
- payment iframe or secure field
- a flow where the provider intentionally requires visible UI confirmation
Do not use browser.act for repeated provider work when a request exists. A chain of UI clicks is slower, harder to test, and more likely to fail after small page changes.
Skill pattern
Keep the public skill stable and hide provider request details in helpers.
export const checkOrder: SkillHandler<{ orderRef: string }> = async ({
input,
browser,
log,
}) => {
const result = await providerApi.getOrderStatus({
browser,
orderRef: input.orderRef,
});
if (!result.ok) {
return {
ok: false,
errorCode: result.errorCode,
message: 'Could not load order status.',
retryable: result.errorCode !== 'provider_rejected',
};
}
log.info('Checked order status', {
orderRef: input.orderRef,
operationName: 'OrderStatusQuery',
});
return { ok: true, data: result.data };
};The skill name and output can stay stable even when the provider changes its underlying endpoint.