Best BQL Practices
We recommend you use the following best practices to make the most out of BQL. This guide covers essential patterns that will make your browser automation more reliable and maintainable.
Prefer Variables Over Hardcoded Values
Use GraphQL variables to keep your BQL operations reusable and easy to maintain. Variables let you parameterize values like URLs, selectors, and input data, so you can avoid hardcoding and repeating them across queries.
- ✅ Good - Using Variables
- ❌ Bad - Hardcoded Values
# Avoiding concatenating strings by using GraphQL variables
mutation LoginUser($email: String!, $password: String!, $loginUrl: String!) {
goto(url: $loginUrl, waitUntil: firstContentfulPaint) {
status
}
typeEmail: type(
selector: "input[type='email']"
text: $email
) {
time
}
typePassword: type(
selector: "input[type='password']"
text: $password
) {
time
}
submitLogin: click(selector: "button[type='submit']") {
x
y
}
}
Then, send a variables
object in your JSON payload (read more about them here)[https://graphql.org/learn/queries/#variables]:
{
"email": "user@example.com",
"password": "securepassword123",
"loginUrl": "https://example.com/login"
}
mutation LoginUser {
goto(url: "https://example.com/login", waitUntil: firstContentfulPaint) {
status
}
# Hardcoding your credentials!
typeEmail: type(
selector: "input[type='email']"
text: "user@example.com"
) {
time
}
typePassword: type(
selector: "input[type='password']"
text: "securepassword123"
) {
time
}
submitLogin: click(selector: "button[type='submit']") {
x
y
}
}
Prefer Built-in Waiters Over Arbitrary Sleeps
Use BQL’s built-in waiters to make your automations more reliable. Smart waiters like waitUntil
and waitForSelector
respond to real page conditions, avoiding the fragility of fixed delays with waitForTimeout
.
- ✅ Good - Built-in Waiters
- ❌ Bad - Arbitrary Sleeps
mutation SearchProducts($url: String!, $searchTerm: String!) {
# Navigate and wait until the network is idle
goto(url: $url, waitUntil: networkIdle) {
url
}
type(text: $searchTerm, selector: "input[name='search']") {
selector
}
click(selector: "button[type='submit']") {
selector
}
# Wait until the result items are present and visible
waitForSelector(selector: ".product-list .product-item", visible: true) {
selector
}
html(visible: true) {
html
}
}
mutation SearchProducts($url: String!, $searchTerm: String!) {
goto(url: $url) {
url
}
# You are assuming everything will be ready in 2.5s
waitForTimeout(time: 2500) {
time
}
type(text: $searchTerm, selector: "input[name='search']") {
selector
}
click(selector: "button[type='submit']") {
selector
}
# Assume results are loaded after 3 seconds
wait2: waitForTimeout(time: 3000) {
time
}
html(visible: true) {
html
}
}
Prefer Semantic Selectors Over Flaky CSS Classes
Prefer semantic selectors for more reliable automations. Use stable HTML attributes, roles, or other meaningful selectors that are less likely to change, and avoid relying on CSS classes that can break with UI updates.
- ✅ Good - Semantic Selectors
- ❌ Bad - Flaky CSS Classes
mutation AddProductToCart($url: String!) {
goto(url: $url, waitUntil: networkIdle) {
status
}
# Use HTML attributes for more reliable scraping
click(selector: "[data-testid='add-to-cart-button']") {
selector
}
# Use HTML roles instead of classes for waiting for a particular selector
waitForSelector(
selector: "[role='status'][aria-label='cart-item-count']"
visible: true
) {
selector
}
html(visible: true) {
html
}
}
mutation AddProductToCart($url: String!) {
goto(url: $url) {
status
}
# Use an unstable, minified class name
click(selector: ".btn-345xy") {
selector
}
# Wait for cart count using a brittle nth-child selector
waitForSelector(selector: ".header > div:nth-child(3) > span", visible: true) {
selector
}
html(visible: true) {
html
}
}
Block Unnecessary Resources
Block unnecessary resources to improve performance. Use the reject mutation to prevent loading ads, images, videos, and other non-essential requests, reducing bandwidth usage and speeding up execution.
- ✅ Good - Block Unnecessary Resources
- ❌ Bad - Loading Everything
# Make sure to enable the Adblock parameter in the editor!
mutation Search($url: String!, $query: String!) {
# Reject images, styles and media right away
reject(
type: [image, stylesheet, media]
) {
time
}
goto(url: $url, waitUntil: networkIdle) {
status
}
type(text: $query, selector: "input[name='q']") {
selector
}
click(selector: "button[type='submit']") {
selector
}
waitForNavigation {
status
}
html(visible: true) {
html
}
}
mutation Search($url: String!, $query: String!) {
# Waits until all images, styles, and requests are done loading
# before continuing
goto(url: $url, waitUntil: networkIdle) {
status
}
type(text: $query, selector: "input[name='q']") {
selector
}
click(selector: "button[type='submit']") {
selector
}
waitForNavigation {
status
}
html(visible: true) {
html
}
}
Use Fragments to Avoid Repeating Query Parts
GraphQL fragments allow you to reuse common field selections and reduce duplication in your queries.
- ✅ Good - Using Fragments
- ❌ Bad - Repeated Query Parts
fragment NavigationResponse on HTTPResponse {
status
time
url
}
mutation ReusableFragments {
homePage: goto(url: "https://example.com", waitUntil: firstContentfulPaint) {
...NavigationResponse
}
loginClick: click(selector: "a[href='/login']") {
selector
}
loginPage: goto(url: "https://example.com/login", waitUntil: firstContentfulPaint) {
...NavigationResponse
}
submitClick: click(selector: "button[type='submit']") {
selector
}
}
mutation RepeatedFields {
homePage: goto(url: "https://example.com", waitUntil: firstContentfulPaint) {
status
time
url
}
loginClick: click(selector: "a[href='/login']") {
x
y
time
}
loginPage: goto(url: "https://example.com/login", waitUntil: firstContentfulPaint) {
status
time
url
}
submitClick: click(selector: "button[type='submit']") {
x
y
time
}
}
Use the proxySticky Parameter
When using GraphQL clients like Apollo or Relay, ensure caching is turned off so browser automation steps always run fresh and return up-to-date results.
- URL Parameter
- Complete Example
https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us&proxySticky=true
mutation StickyProxyExample {
goto(url: "https://httpbin.org/ip", waitUntil: firstContentfulPaint) {
status
}
firstIP: text(selector: "pre") {
text
}
goto(url: "https://httpbin.org/headers", waitUntil: firstContentfulPaint) {
status
}
headers: text(selector: "pre") {
text
}
}
Benefits of proxySticky:
- Consistent IP address across the entire session
- Reduces chance of being flagged for IP switching
- Better for multi-step workflows requiring session continuity
Disable Mutation Caching in GraphQL Clients
When using GraphQL clients like Apollo or Relay, disable caching for BQL mutations since browser automation results are not cacheable and should always execute fresh.
- Apollo Client
- Generic GraphQL Client
import { ApolloClient, InMemoryCache, gql } from '@apollo/client';
const client = new ApolloClient({
uri: 'https://production-sfo.browserless.io/chromium/bql?token=YOUR_TOKEN',
cache: new InMemoryCache()
});
const BQL_MUTATION = gql`
mutation ScrapePage($url: String!) {
goto(url: $url, waitUntil: firstContentfulPaint) {
status
}
content: text {
text
}
}
`;
const result = await client.mutate({
mutation: BQL_MUTATION,
variables: { url: 'https://example.com' },
fetchPolicy: 'no-cache'
});
const response = await fetch('https://production-sfo.browserless.io/chromium/bql?token=YOUR_TOKEN', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-cache, no-store, must-revalidate'
},
body: JSON.stringify({
query: `
mutation ScrapePage($url: String!) {
goto(url: $url, waitUntil: firstContentfulPaint) {
status
}
content: text {
text
}
}
`,
variables: { url: 'https://example.com' }
})
});
Summary
The tl; dr is:
- Use variables to make queries reusable and easy to maintain.
- Rely on built-in waiters for intelligent waits based on real page conditions.
- Choose semantic selectors over CSS classes for greater stability.
- Block unnecessary resources to improve performance and reduce costs.
- Use fragments to avoid duplication and keep queries clean.
- Configure sticky proxies for consistent IP addresses and improved stealth.
- Disable client-side caching so browser automation always runs fresh.
For more advanced techniques, explore the BQL Recipes section and refer to the API Reference for complete mutation documentation.