Building an answering machine with superpowers: Claude, AWS Connect, German bureaucracy
May 6, 2026Working on our website, it occurred to me — we could just replace our phone number with a simple answering machine.
As a good blogger, I’m looking for examples to showcase our own line of work applied to ourselves. So I quickly made up the idea of: let’s hook up AWS Connect to a phone number, have a bit of answering-machine friendliness and in urgent cases forward the call to me.
Also I don’t have time for this and should have delegated the job to somebody more capable. Or just chosen one of the readymade SaaS solutions. But that ruins the fun.
So to live up to my prognosis I quickly fired up Claude Code and chose superpowers (out of the current crop — bmad, spec-kit, openspec, et al.).
Once installed, superpowers just turns on — no manual invocation, the right skill fires for the right step. The iteration speed is extremely well done: brainstorm → spec → plan → code happens in flow, with the correct gate at each transition, no faffing about with which command to type next.
The plan, courtesy of superpowers:brainstorming → writing-plans
I told superpowers up front to use CDK, and fed it two repos I’d built before that match my style. (Side note: a lot of those patterns are copies of things I picked up from projen — which is, fittingly, a generator. Generators all the way down.) That priming did most of the heavy lifting on getting the structure right.
The flow is: brainstorm → spec → plan → execute. I fed Claude the rough idea, it walked me through clarifying questions one at a time (German prompts? voicemail length? who’s the MD?), then dropped a 200-line design doc and a step-by-step plan. Both got committed before any code was written:
4f6918f docs: add design spec and implementation plan
That worked great. Three CDK stacks, DTMF menu, KVS → S3 → Transcribe → Slack pipeline, alarming, the works. Tests written alongside (superpowers:test-driven-development was on, so jest tests for every Lambda landed in the same commits).
Timeline
OK, full disclosure before I write this section: it wasn’t days, it was minutes, and it was at night. So the honest version:
- Night 1 — brainstorm + spec + plan + scaffold + three stacks + four Lambdas + tests + GitHub Actions. A handful of hours.
- Night 2 — deploy. This is where it got fun.
- Day 3 — already writing this blog post. Yes, including the bit you’re reading right now.
How this even became a success, I’m not sure. But it made me stay curious — by the time something should have made me stop, the next thing was already half done.
While doing that, it occurred to me that I also need a fresh AWS account for our org — that’s how we separate workloads. Getting one from Control Tower actually took longer than for Claude to complete the entire CDK stack.
After a bit of fiddling and connecting it, I tried to deploy. It didn’t work.
The four boss fights
1. The “fake” AWS L2 constructs
Claude wrote idiomatic CDK using @aws-cdk/aws-connect’s L2 constructs — Instance, ContactFlow, that sort of thing. Pleasant API, looks plausible. Doesn’t exist. There’s an AWS blog post that hints at L2 constructs for Amazon Connect, but the package only ships CfnInstance, CfnContactFlow etc. as L1 wrappers.
We dropped down to L1, kept the structure, moved on.
2. “Just claim a German number from the Connect pool” — said the spec
Then I noticed: claiming a DE number takes 2 to 6 weeks, up to 60 days, and requires a Handelsregister-Auszug issued in the last 6 months, uploaded via a separate AWS Support case. Not “instant claim from a pool” like the spec naively had it.
Filed the support case. Started looking for an interim solution.
3. US toll-free is not dialable from Germany
Plan B: claim a US toll-free as a stand-in. CDK-managed, claims instantly, no paperwork. Forward our DE number via 1&1 to the TFN. Test the flow today, swap to DE DID once approved.
The TFN claimed fine. I called it from my mobile and got:
“Die gewählte Rufnummer ist vorübergehend nicht erreichbar.”
US 1-8xx numbers are restricted to calls originating in North America. The carrier intercepts before the call ever leaves Germany.
The full plan had been: caller dials our regular DE number → 1&1 Rufnummernumleitung forwards it transparently to the US TFN → AWS Connect picks it up → German prompts, voicemail, the works. I’m only realising as I write this that the Rufnummernumleitung itself goes out via DTAG and would have hit the exact same intercept I did. There was never a path. Plan B was dead before I called it.
4. CFN’s AWS::Connect::PhoneNumber provider for DIDs
Plan C: claim a US local DID instead. Same code, change type: 'TOLL_FREE' → type: 'DID'. CDK redeploy.
UPDATE_FAILED AWS::Connect::PhoneNumber PhoneNumberDID
"did not stabilize." HandlerErrorCode: NotStabilized
The handler reports failure after ~5 seconds. The rollback’s DELETE_COMPLETE then takes 2 minutes 51 seconds — which is how long the actual claim took. So the claim was succeeding on Connect’s side; AWS’s CloudFormation provider is just too impatient to wait for it. TFN inventory is preallocated and claims in <5s; DIDs involve carrier-side number assignment.
There is no parameter to make CFN wait longer. The AWS::Connect::PhoneNumber resource is fundamentally broken for any non-TFN claim. Time to bypass it.
What ended up working
A small AwsCustomResource-style Lambda that claims via connect:ClaimPhoneNumber and polls DescribePhoneNumber until status is CLAIMED. Done. AWS’s error messages around Connect are sometimes load-bearing fiction; you learn to bypass them with custom resources.
What I learned
It takes some planning and thinking. You can’t just spit the thing into prod.
Overall I was more or less familiar with the domain so there were no real surprises about the architecture itself. I had to guide it a bit around CDK multistack stuff — cross-stack references via SSM, ordering, what survives a rollback, when CDK resource replacement actually means a new physical resource. Most of the loops we had to do were on AWS being AWS, not on Claude being Claude.
AWS Connect is UI-driven at its core. The API exists, but the moment you wire things together from CloudFormation you find missing resources, undocumented races, and behaviour only the admin console knows about. Managed service, manage it yourself.
Picked up a bit about Slack automation again on the way. Lots of channels and posting options lead straight to paralysis of choice — which is something superpowers is really good at tackling in the design phase, by forcing you to commit to one path before any code gets written. I’d dabbled with this when setting up my half-orphaned Hermes agent, but that’s another story.
A genuinely bad decision: renaming the project from “ivr-responder” to “answering-machine” after the spec, plan, and most of the code had been generated. Roughly 15 minutes and a bunch of tokens to chase the rename through every file, test, doc, and stack name. Pick the name early.
Also: having a live GitHub Action with autodeploy on main while you’re still firefighting the first deploy is confusing, to say the least. But that’s left to Captain Obvious.
What I’d do differently
- Build the contact flow visually first, dump it via
aws connect describe-contact-flow, encode that as JSON into the repo. The spec walked Claude into hand-writing flow JSON from docs; some of the parameter names are wrong (Waitis chat-only;MaximumDigitsshould beMaximumLength; etc.). The Connect admin UI generates valid JSON for free. - Not promise myself “and then we’ll get a German number.”
- Maybe consider one of those SaaS solutions next time. But what would I have to write about then.
Code’s on GitHub if you want to poke at it: superluminar-io/answering-machine.
Whoa. This is what you get from deliberately engaging with things: a whole lot. And that was a small excerpt — now imagine doing this with something serious and a team.
Talk to us. Thanks :)
