Skip to main content

Facebook is now programmable. All of it

Everything you do by hand in the ad account, a script can now do for you — through your own profile, on a schedule, around the clock.

This is Facebook Qubix Bridge — a direct line into Facebook from your Qubix scripts. Your script sends its own request to Facebook — to the Graph API or to any other Facebook address — and Qubix carries it through your browser profile: its key, its cookies, its proxy, its browser signature. The idea that yesterday sat in someone else's development queue is today a short script the assistant writes for you.

🎬 GIF: a script reads a campaign's daily budget, raises it, and the change shows up in the ad account

What this opens up

🧠 Your funnel decides — your profile acts. A script reads payback straight from your own statistics with a SQL query — deposits, revenue, any slice down to geo and hour — and moves the ad account by it. The ad account itself cannot do this in principle: it has never seen your funnel.

💰 Budgets on your own logic. The script finds campaigns paying back above your threshold, reads their live budget from Facebook itself, and raises it by your step — with a ceiling and a cooldown, so growth stays smooth and learning is left alone. A ready recipe, "raise the budget on profitable campaigns", already ships with the product: copy it and set the thresholds to your own.

🔍 Diagnosis in Facebook's own words. "The ad is active, but nothing is spending" — the script asks Facebook directly and brings back its own wording of the problem: rejected in review, the ad set has ended, the account is restricted.

💳 Ad account money, as one summary. Total spend, daily limit, billing threshold, the card on file — for every account, on a schedule, straight into the console or as a message to you in the messenger.

🎯 The settings that never show up in reports. Ad set targeting, optimisation goals, the nested settings of an ad — all read in a single request.

📦 Hundreds of objects in one exchange. A batch of requests goes to Facebook at once: the budgets of every campaign come back in a single trip rather than one by one.

🖼 Creatives, brought home. Images and video sources are pulled down by the script — the files themselves — and stay with you.

🧹 Comments, by your own rules. Qubix already clears planted links on its own; the bridge adds your logic on top: your own word lists, your own exceptions.

📊 Ad account statistics in any slice — when you need a cut that no ready-made report has, the script asks Facebook for it itself.

And that is only the start of the list: the bridge accepts any Facebook address — the Graph API and beyond, down to binary exports. Your imagination is the only thing setting the scope here — that, and the rights your own profile already has in Facebook.

How it works: a pipe, and only a pipe

You write the address, the method, the parameters and the headers, while Qubix supplies the key, the cookies, the proxy and the browser signature. There are deliberately no ready-made campaign-management functions in the bridge — they would freeze today's shape of Facebook into the product and break at its first change. The bridge knows nothing about the shape of a request — which is exactly why it does not go stale: Facebook changes a field, and you edit a line in your own script without waiting for a Qubix update.

The fb command is available in user scripts — both on a schedule and via the ▶ Run button.

Two ways to choose the executor profile

Every request is sent by some browser profile — the executor. You either name it yourself or let Qubix pick one.

JavaScript
// a) let Qubix pick: any profile that sees this campaign
const res = fb.campaign(campaignId).request({
method: 'GET',
url: `https://graph.facebook.com/${campaignId}`,
params: { fields: 'name,daily_budget' },
})

// b) a named profile: no picking, no fallbacks
const res2 = fb.profile(profileId).request({
method: 'GET',
url: 'https://graph.facebook.com/me',
params: { fields: 'id,name' },
})
  • fb.campaign(campaignId) — Qubix picks the executor among the profiles that see this campaign, preferring the profiles of the ad's owner. If a candidate drops out on the delivery level, the next one is tried — but only for reads: a change is never retried through another profile, so it cannot be applied twice. As soon as Facebook answers — even with a refusal — the search stops.
  • fb.profile(profileId) — the request goes through exactly this profile, with no fallbacks. The list of profiles that see a campaign or an ad comes from getProfiles() — with everything we know about them: the name, the group, the owner, whether the token is alive, whether the profile is stuck on a security checkpoint.

The request and the answer

request(options) takes one object:

OptionMeaning
urlthe absolute address of the request: `https://graph.facebook.com/${campaignId}`, https://graph.facebook.com/me/adaccounts. Any *.facebook.com host works — the profile's cookies and key never go to a foreign host
methodGET (default), POST or DELETE
paramsrequest parameters as an object; POST carries them in the body, the other methods — in the address
headersyour own headers; they go on top of ours and may override any
timeoutMshow long to wait for the answer; empty — the operator's default, and you cannot go above the ceiling

Facebook's answer arrives as is, Qubix does not parse it — you do: status (there is deliberately no ok field — it could only ever be true and would hide refusals), contentType, headers (all except Set-Cookie), text (the body as a string — JSON.parse(res.text)), bytes (the same body in binary — for images and downloads), profileId (which profile sent the request).

There are two kinds of refusal: Qubix could not deliver — thrown as an error, catch it with try/catch; Facebook answered with a refusal — a normal answer with status >= 400 and the reason in the body. A three-line helper so you do not repeat the parsing:

JavaScript
function graph(res) {
if (res.status >= 400) throw new Error('Facebook refused: ' + res.status)
return res.text ? JSON.parse(res.text) : {}
}
Do not re-read right after a change

Facebook serves reads with a delay: a value read immediately after a successful change may still be the old one. Check on the next run, or remember what you set in ctx.state.

Batch: many requests in one exchange

The per-profile queue lets one request through per interval — a hundred single reads will not fit into the run's time limit. A batch pays the queue once:

JavaScript
const rows = fb.batch(fb.profile(profileId), ids.map((id) => ({ method: 'GET', relativeUrl: `${id}?fields=name,daily_budget` })))
for (const r of rows) {
if (r.code >= 400) { console.log('code', r.code); continue }
console.log(r.data.name, r.data.daily_budget)
}

fb.batch slices by 50 subrequests on its own — that is Facebook's limit; the order is preserved, each row carries its own code (a refusal is code >= 400) and the parsed answer in data. A subrequest is { method, relativeUrl, body }; a list of reads is built with a plain map. A batch never falls back to another profile — a change inside it is applied once or not at all.

The catalog of building blocks

Every request below has been sent by Qubix in live operation and answered (verified on 27 August 2026) — you can rely on them as the base set. Anything beyond also goes through: the bridge accepts any *.facebook.com address, and your profile's rights decide the rest.

Who am I under this profile

JavaScript
const res = fb.profile(profileId).request({ url: 'https://graph.facebook.com/me', params: { fields: 'id,name,email' } })

A refusal here means the profile's session is dead — the cheapest way to check session health on a schedule, earlier than the media buying notices.

Ad accounts — including money

JavaScript
const res = fb.profile(profileId).request({
url: 'https://graph.facebook.com/me/adaccounts',
params: {
limit: 50,
fields: 'name,account_id,account_status,disable_reason,currency,' +
'adspaymentcycle,adtrust_dsl,amount_spent,timezone_name',
},
})

The money lives here — the Graph API has no separate "balance" request: amount_spent is the account's total spend in minor units of its currency, adtrust_dsl the daily spend limit, adspaymentcycle the billing threshold (the amount sits in .data[0].threshold_amount and is divided by 100), account_status and disable_reason the state and the disable reason, as numbers. Payment cards — the same address with the field all_payment_methods{pm_credit_card{display_string}}.

Campaign state and budget

JavaScript
const res = fb.campaign(campaignId).request({
url: `https://graph.facebook.com/${campaignId}`,
params: { fields: 'id,name,status,effective_status,daily_budget' },
})

daily_budget comes in minor units, as a string. If the budget is set on the ad sets, the field is simply absent — a normal state, not an error.

Changing the budget

JavaScript
const res = fb.campaign(campaignId).request({
method: 'POST',
url: `https://graph.facebook.com/${campaignId}`,
params: { daily_budget: '5000' }, // minor units: 5000 = 50.00 in the account currency
})

On success Facebook answers {"success":true}.

Ad set settings: targeting, budgets, optimization

JavaScript
const res = fb.campaign(campaignId).request({
url: `https://graph.facebook.com/${adsetId}`,
params: { fields: 'name,targeting,daily_budget,lifetime_budget,optimization_goal,billing_event' },
})

The same way, one request on an ad pulls its nested objects — the campaign, the ad set and the creative at once: fields: 'name,effective_status,campaign{objective,daily_budget},adset{targeting},creative{id}'.

Pausing and starting

JavaScript
fb.campaign(campaignId).request({ method: 'POST', url: `https://graph.facebook.com/${someId}`, params: { status: 'PAUSED' } })
fb.campaign(campaignId).request({ method: 'POST', url: `https://graph.facebook.com/${someId}`, params: { status: 'ACTIVE' } })

One form for an ad, an ad set and a campaign. Pausing a campaign cascades: its ads report effective_status: 'CAMPAIGN_PAUSED' while their own status stays unchanged — judge by effective_status.

Why an ad is not delivering

JavaScript
const res = fb.campaign(campaignId).request({
url: `https://graph.facebook.com/${adId}`,
params: { fields: 'effective_status,issues_info{error_summary,error_message,level},adset{effective_status,end_time}' },
})

issues_info carries the problem in Facebook's own words; adset.end_time catches an expired ad set.

Statistics straight from the cabinet

JavaScript
const res = fb.campaign(campaignId).request({
url: `https://graph.facebook.com/act_${accountId}/insights`,
params: {
level: 'ad',
fields: 'ad_id,ad_name,spend,impressions,clicks',
time_range: JSON.stringify({ since: '2026-08-20', until: '2026-08-27' }),
time_increment: 1,
limit: 500,
},
})

Two traps: outbound_clicks arrives as an array of objects, and there is no top-level landing_page_views field at all — look for it inside actions.

Creatives, pages, comments

JavaScript
// images by hash
fb.campaign(campaignId).request({ url: `https://graph.facebook.com/act_${accountId}/adimages`, params: { hashes: JSON.stringify([imageHash]), fields: 'url,permalink_url,hash' } })
// the source file of a video
fb.campaign(campaignId).request({ url: `https://graph.facebook.com/${videoId}`, params: { fields: 'source' } })
// fan pages and business managers — independent reads, one exchange
const rows = fb.batch(fb.profile(profileId), [
{ method: 'GET', relativeUrl: 'me/accounts?limit=25' },
{ method: 'GET', relativeUrl: 'me/businesses' },
])
// posts of a page
const posts = graph(fb.profile(profileId).request({ url: `https://graph.facebook.com/${pageId}/published_posts`, params: { limit: 5 } }))
// comments of ALL fresh posts — one batch for the whole list
const coms = fb.batch(fb.profile(profileId),
(posts.data || []).map((po) => ({ method: 'GET', relativeUrl: `${po.id}/comments?fields=id,message,from,created_time&limit=100` })))
// delete a comment — a change goes as a single request
fb.profile(profileId).request({ method: 'DELETE', url: `https://graph.facebook.com/${commentId}` })

An fb answer can be binary too: res.bytes carries the body as is — that is how you take the files themselves, not just links to them. Facebook serves long lists with cursors: while the answer carries paging.next, take paging.cursors.after and repeat the request.

The power is in combining with the rest of the SDK

Conditions select objects by your statistics, sql pulls any slice from the database, fb checks and changes the cabinet, ctx.state keeps memory between runs, ctx.fetch sends the result to your messenger:

JavaScript
function main() {
// 1. Your own statistics — a direct ClickHouse query: campaign payback by FUNNEL revenue
const rows = sql`
SELECT campaign_id, sum(spend_24h) AS spend, sum(revenue_24h) AS revenue
FROM v_ads_stats
GROUP BY campaign_id
HAVING spend > 0 AND revenue / spend >= 1.5
ORDER BY revenue / spend DESC`

// 2. Live budgets of all candidates — in one exchange with Facebook
const ids = rows.map((r) => r.campaign_id)
if (!ids.length) return
const budgets = fb.batch(fb.campaign(ids[0]), ids.map((id) => ({ method: 'GET', relativeUrl: `${id}?fields=name,daily_budget` })))

for (let i = 0; i < ids.length; i++) {
if (budgets[i].code >= 400) continue
console.log(budgets[i].data.name,
'funnel ROAS:', (rows[i].revenue / rows[i].spend).toFixed(2),
'budget in the cabinet:', budgets[i].data.daily_budget)
}
}

This is how you build scenarios a vendor would never get around to: your funnel decides, your profile acts.

You may never write a line of code

Describe the job in plain words — "every morning, raise the budget on campaigns that pay back, but never past the ceiling" — and the AI assistant, right there in the editor, assembles the script, drops it into the code and helps you put it on a schedule. The assistant knows both the bridge and your statistics: a script can first ask your funnel which campaigns brought deposits, and then go to Facebook and move their budgets.

🎬 GIF: a task in plain words in the assistant → a ready script in the editor → the run button

Every step stays under your eye

Every call a script makes to Facebook — reads included — is written to the log on your own server: which script, when, through which profile, and what Facebook answered. A change is never retried through a backup profile, so it can never be applied twice. Requests to one profile are spaced out in time alongside all the rest of the automation — Britva, statistics collection and scripts share one queue, so the profile stays safe. Script runtime cuts fb calls like the rest of the code, the per-call wait is capped by the operator's ceiling, and access keys never reach the log at all — they are scrubbed from stored texts.

Nothing to set up

The script goes to Facebook through your own browser profile — the very one you work in: its key, its cookies, its proxy, its browser signature. To the platform this looks like a live person at work rather than automation. The profile is connected to Qubix — the bridge is already running.


Every "could this just happen by itself?" used to run into somebody else's hands. Now the only thing between your idea and working automation is one script, and the assistant writes it for you.

Update Qubix and hand the ad account routine over to scripts. 🚀

What's next