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.
// 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 fromgetProfiles()— 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:
| Option | Meaning |
|---|---|
url | the 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 |
method | GET (default), POST or DELETE |
params | request parameters as an object; POST carries them in the body, the other methods — in the address |
headers | your own headers; they go on top of ours and may override any |
timeoutMs | how 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:
function graph(res) {
if (res.status >= 400) throw new Error('Facebook refused: ' + res.status)
return res.text ? JSON.parse(res.text) : {}
}
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:
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
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
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
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
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
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
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
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
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
// 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:
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. 🚀