A few weeks ago I built a scheduler into Documentor (my content tool). AI generates a post, I pick a time, and at the scheduled minute it goes live on the Facebook page. Looked perfect on paper.

I used it for a week and something felt off.

Some posts — the AI hook was fine but I'd want to tweak the first line before it went out. Other posts — the caption was too long. Sometimes the image color wasn't quite the vibe I wanted. To change anything I had to open Documentor on my laptop, edit, unschedule, reschedule. Friction every time.

What I actually wanted wasn't "AI auto-posts on a timer." What I wanted was "AI prepares a draft, I review on my phone, I hit publish." Those are very different products.

So I told Tim: "Kill the scheduler. Replace it with one button: 'Create FB Draft'. The post should land in Meta Business Suite under Drafts. I'll review on mobile and publish myself."

"Got it." He started refactoring.

Layer 1 — published=false sent the post live anyway

Easy bit first. The existing code called POST /page/photos with published=true. Tim flipped it to published=false, expecting that to make a draft.

API returned 200 OK + a post_id. Looked clean. I tested it — opened Meta Business Suite on my phone → Content → Drafts.

Empty.

I checked the page directly. No new post, published or unpublished. The post_id was real, the API said it existed, but no UI surface in Facebook would show it.

Tim didn't wait for me to ask. He fired a Graph API request at the post_id directly:

GET /{post_id}?fields=is_published,full_picture,permalink_url

The post existed with is_published: false, but it was sitting in something called the "Unpublished Photos album" — a hidden bucket Facebook uses for photos that have been uploaded but never attached to a feed post. That's not the same thing as a draft post. Business Suite's Drafts tab only shows the second one.

Tim's exact line back: "FB has two separate concepts here — unpublished photo (a photo that was uploaded but not posted) versus draft post (a feed post saved as draft). Business Suite Drafts only shows the second. We need to do this in two steps."

Layer 2 — the two-step flow Facebook itself uses still didn't show up

Tim rewrote it the way Facebook's own UI does it under the hood:

  1. POST /page/photos with published=false and no caption → returns photo_id
  2. POST /page/feed with published=false, attached_media[0]={media_fbid: photo_id}, and the caption → returns a new post_id that is a real draft post, not a stray photo

Deployed to production. I tested again. Both API calls returned 200 OK.

Opened Business Suite → Drafts.

Still empty.

(I was starting to lose patience, not gonna lie.)

Tim didn't blink. He pulled the new post_id and queried Graph API for everything it knew about it. The post had is_published: false ✓, the caption ✓, the photo attached ✓ — every field looked correct. But Business Suite still wouldn't surface it.

So he started comparing fields between two posts: one I'd manually saved as a draft from inside Facebook's own UI on my phone, and the one his API call had created. He pulled both with the same Graph query and diffed the response.

One field was different. One.

The UI-created draft had a field called unpublished_content_type. The API-created draft didn't.

Layer 3 — an undocumented field that flips Business Suite's filter

The publicly documented Facebook Graph API does not mention that unpublished_content_type is a parameter you can pass when posting to /feed. It shows up in responses, never in the parameter list. Nowhere does it tell you the values it can take or what they mean.

The default value Facebook quietly assigns is INLINE_CREATED — meaning "post created externally, via API." Business Suite's Drafts tab is intentionally filtered to hide these. They show up nowhere visible to a normal page admin.

The value Business Suite does show is DRAFT.

Tim added one line to fb_poster.py:

feed_data = {
    "access_token": access_token,
    "published": "false",
    "unpublished_content_type": "DRAFT",  # ← layer 3
    "attached_media[0]": json.dumps({"media_fbid": photo_id}),
    "message": caption,
}

Deployed. I hit Re-push on one of the orphan drafts still sitting in our DB.

Opened Business Suite → Drafts.

There it was.

I tapped edit, tweaked the hook, hit Publish. Post went live on the page exactly the way I'd wanted from the start.

Total time — one afternoon

The whole thing happened in a single afternoon. Tim worked through it like this:

  • 2:00pm — I asked him to delete the scheduler and build Create FB Draft instead
  • 2:30pm — Schema refactor done (deleted scheduler.py, dropped the scheduled_at column, removed the Scheduled tab from the UI, added a new status: posted_draft)
  • 2:50pm — Deploy 1 (single-step published=false) — discovered the Unpublished Photos album problem
  • 3:15pm — Deploy 2 (two-step photo+feed) — Business Suite still didn't show the draft
  • 3:40pm — Spotted unpublished_content_type=DRAFT by diffing fields — fixed the third layer
  • 4:00pm — Added a Re-push button for the drafts that had been orphaned by my testing, plus a Drafted tab in the Documentor UI so I can see what's waiting → done

If I'd done this myself I'd have lost two or three days. Most of that time would've gone to Googling "FB Drafts API not showing in Business Suite" and reading Stack Overflow threads where people describe the exact problem and then never get an answer. (I went and Googled afterwards out of curiosity — there is a forum post that names the missing field. It's in Russian.)

Tim didn't Google any of it. He used the Graph API directly, compared his output to the UI's output, and found the diff. "Whatever Facebook's own UI can do, I can do — if I can see the response fields, I can spot what's different."

What I'm taking away from this

1. The workflow I want is rarely "automation." It's "AI does the homework, I sign off."

I keep rediscovering this. I thought I wanted a scheduler because it sounded more "advanced." In actual use I wanted a draft button — which is just AI-prepared work waiting on my approval.

Same lesson as that post a while back about not letting AI publish content unsupervised. AI prepares; I decide. The system should match the workflow I actually use, not the one I imagined when I designed it.

2. Delete the feature you don't actually use. Don't keep it "just in case."

Tim didn't just add a Draft button and leave the scheduler sitting there as a fallback. He deleted scheduler.py, dropped the scheduled_at column, and removed the Scheduled tab from the UI entirely. Done.

Same instinct that drove him to strip 250 lines of his own alert system a couple months back. He doesn't get attached to code that doesn't serve a real workflow.

3. Trust the source of truth, not the SDK response.

If Tim had stopped at "API returned 200 OK," he would have stopped at layer 1. He didn't. He opened Graph API explorer, queried his post directly, then queried a UI-created post for comparison, then diffed.

Same pattern as when he tracked down a missing Stripe webhook — didn't trust the dashboard, queried the DB. Same pattern as when he fixed my idle-customer metric — didn't trust the dashboard number, SSHed into customer servers and looked at file timestamps.

"Trust but verify" is what makes a good AI agent different from a chatbot that just believes whatever the SDK said.

4. A real AI agent can dig out undocumented behavior.

Facebook's docs don't mention unpublished_content_type=DRAFT as a valid parameter for /feed. It's pure undocumented behavior — the kind you only find by trial and comparison.

A normal ChatGPT session can only reflect back what's in the docs. An AI agent that has Graph API access, sees my actual code, and can deploy and re-test on its own can dig through three layers of undocumented quirks in a couple hours. That's a different category of tool.

Why I'm sharing this

Two takeaways for anyone running a one-person business:

One. Tools you build yourself beat SaaS because workflows shift. I thought I needed a scheduler — turns out I needed a draft button. If I'd been paying a SaaS for "AI Facebook scheduling" I'd be filing a feature request, hoping they care, waiting six months. Because I built Documentor on my own server with my own AI agent, I told Tim what changed and he had it shipped that afternoon. (More on this idea in "Why I Build My Own Tools Instead of Paying for SaaS".)

Two. Bugs that go three layers deep into a third-party API normally eat a full day. Tim closed this one in two hours because he had SSH into my server, the FB Graph API token, the source code in front of him, and the ability to deploy and re-test on his own — no waiting for me to answer questions, no permission requests, no environment setup. Deploy → test → query the new field → compare → fix → redeploy. The loop ran fast because nothing in the loop required a human in the middle.

If I'd hired a freelance developer to do this, they'd have done it eventually — but two or three days in, between onboarding, environment setup, asking me for tokens, and waiting on me between iterations.

Try having your own AI agent for a week

Tim isn't a special model. He's Claude Code, running on a server I own, with access to every codebase, every API key, every database I have. That's the entire setup.

Newton gives every customer the same setup — your own private server, your own AI agent with its own SSH access, your own keys. It can dig three layers deep into someone else's undocumented API and ship the fix the same afternoon, because the only thing slowing this kind of work down is friction, and an agent on your own infrastructure has none. Try Newton free for 7 days and see what your AI gets done while you're not watching.

(And next time I tap Create FB Draft in Documentor, the post lands in Business Suite first try. Thanks Facebook for shipping a maze of an API so Tim could show off.)

— Pond