GTM automation without the deep end.
Your agent writes the plays. Shallow Curve runs them on Cloudflare, straight off your PostHog data, with your own API keys. No webhook for every event. No credits to babysit.
Some tools draw a deep line. We drew a shallow curve.
Depth below sanity, first six weeks of GTM automation. Data is anecdotal but heartfelt.
The deep line
- Gave the GTM engineer the whole Postgres
- Webhook number 4,812
- Rebuilt the PostHog funnel. Again.
- Out of credits, mid-waterfall
The shallow curve
- Pointed it at PostHog
- Your agent wrote the play
- Pipeline running while you sleep
What changes when you leave the deep end
- Getting your data in
- The deep end: Give the GTM engineer your entire Postgres. Then fire a webhook every time a user so much as blinks.
- Shallow Curve: Connect PostHog, or pick the Postgres tables you are happy to share. New rows start plays on their own.
- Paying for lookups
- The deep end: Buy credits. Watch credits. Run out of credits halfway through a waterfall at 2am.
- Shallow Curve: Bring your own API keys and pay each provider directly. We add no markup and run no meter.
- Where it runs
- The deep end: Somewhere. Probably fine.
- Shallow Curve: Cloudflare Workers, Durable Objects, D1 and R2. A play can sleep for a week and costs nothing while it naps.
- The plays you already wrote
- The deep end: Stuck where they are.
- Shallow Curve: Same authoring API. Change one import, port the SQL, carry on.
A play is one TypeScript file
Your coding agent writes it, you read it, and it runs every time PostHog says someone signed up. Every step is saved as it finishes, so a run that crashes picks up where it stopped instead of paying for the same lookup twice.
These two commands are the entire PostHog integration:
sc sources add posthog --project 12345 --events "user signed up" sc plays trigger event new-signup \ --table posthog.events --where "event = 'user signed up'"
new-signup.play.tswritten by your agent, reviewed by you
import { definePlay } from 'shallowcurve';
export default definePlay('new-signup', async (ctx, user) => {
const site = await ctx.tools.execute({
id: 'homepage',
tool: 'contextdev_get_web_scrape_markdown',
input: { url: `https://${user.domain}` },
});
const tier = await ctx.tools.execute({
id: 'tier',
tool: 'ai_inference',
input: { prompt: tierPrompt(site), jsonSchema: TIER },
});
await ctx.sleep(2 * HOUR); // a nap, not a server bill
return { tier: tier.extractedValues.json.get() };
});- Providers wired up
- 126
- Tools your agent can call
- 1,506
- Webhooks you have to write
- 0
You bring the API keys. We bring the plumbing and never mark it up.
Wade in.
It’s shallow. That’s the whole point.