AWS Builder Center metadata — Aubade companion (Builder Log Part 2 companion)
A companion to Logesh's Part 2: the one engineering lesson from his autonomous creative AWS build, where a Bedrock critic proposes but a fitness function decides.
The Other Half of Our Weekend Challenge: Why Logesh Made the Code Decide
Builder Log, Part 2 companion
The first post in this series promised a second build from the same AWS Builder Center July Weekend Challenge.
That build is now live. Logesh built Aubade, an autonomous creative studio that produces a new morning composition without somebody opening a dashboard, choosing a prompt or approving the result.
I am not rewriting Logesh's technical walkthrough here. His article is the canonical Part 2 and the repository contains the implementation. This is my perspective on the design choice I found most important while looking at the build: the model proposes, but the code decides.
Original Part 2: [add Logesh's AWS Builder Center article URL ]
Source code: Aubade on GitHub
Live gallery: d2bq7r7f8dyh8z.cloudfront.net
The Weekend Challenge connection
Logesh and I entered the same July Weekend Challenge and both finished in the first 100. I built the Daily GitHub Explorer, an AWS agent that wakes up, finds a recent repository, summarizes it with Amazon Bedrock and publishes the result.
Logesh solved a different problem. He wanted to see whether an agent could create something every morning and improve its style over time without becoming unpredictable.
That difference matters. A daily briefing can be judged against facts such as the repository name, language and star count. A creative composition needs a quality signal, a memory of what worked and a way to explore without drifting into nonsense.
Aubade is Logesh's answer to that problem.
What Aubade does
Once a day, Amazon EventBridge Scheduler invokes the agent. The run chooses the next city in its rotation and reads the current sky from Open-Meteo. Amazon Bedrock Nova Pro acts as the art director. It proposes a palette, mood, poem and a small change to the style genome.
The system then creates four candidate variants:
- Champion: yesterday's winning style, unchanged
- Directed: the champion plus today's weather-driven suggestion
- Critic: the champion plus the previous critic's suggested mutation
- Explore: the champion plus a deterministic random walk
Each candidate is rendered and scored in code. The highest-scoring candidate becomes the next champion. The selected composition is published as a vector poster and a raster plate. Amazon Polly narrates the poem, and the day's artefacts and record are published through Amazon Simple Storage Service (Amazon S3) behind Amazon CloudFront.
The critic can suggest a change for tomorrow, but its suggestion is not automatically trusted. It has to beat the alternatives on the objective function before it becomes part of the style.
That is the core idea.
Architecture
The repository's deployed shape is a scheduled serverless workflow. There is no web request that starts a run and no human approval step in the normal path.
1
2
3
4
5
6
7
8
9
flowchart LR
Scheduler[Amazon EventBridge Scheduler\n06:00 UTC] --> Lambda[AWS Lambda\nPython 3.12, arm64]
Lambda --> Weather[Open-Meteo\nweather input]
Lambda --> Director[Amazon Bedrock\nNova Pro art director]
Lambda --> Critic[Amazon Bedrock\nNova Lite critic]
Lambda --> Polly[Amazon Polly\npoem narration]
Lambda --> State[Amazon DynamoDB\nstyle genome + archive]
Lambda --> Assets[Amazon S3\nposters, plates, audio, JSON]
Assets --> CDN[Amazon CloudFront\npublic gallery]Figure 1: Aubade's scheduled creative loop, reconstructed from the public repository.
The important boundary is between the Bedrock calls and the renderer. Bedrock supplies direction and language. The renderer owns the pixels and the SVG. The fitness function owns selection. DynamoDB stores the surviving genome and the daily archive so the next invocation has memory without relying on the model to remember yesterday.
Why the boundary matters
My first reaction to the design was that it treats a generative model as one component, not as the whole application.
That sounds obvious, but it is easy to build an autonomous system around the assumption that a model's output is the decision. In Aubade, the model's output is a candidate input to a constrained program.
The repository keeps the style genes bounded in
src/genome.py. The renderer in src/art.py produces the flow-field composition. The fitness function in src/fitness.py measures the candidates. The handler coordinates the morning run in src/handler.py.That separation gives the system three useful properties:
- A bad model value cannot become an unlimited drawing instruction. Values are sanitised and constrained before they reach the renderer.
- The output is reproducible enough to debug. The deterministic engine can render the same style inputs again instead of asking a model to redraw the same idea from scratch.
- Improvement has a definition. A mutation survives because it scores better than the alternatives, not because a critic described it confidently.
This is the same pattern I keep reaching for in agent systems: put the model on the side of the boundary where ambiguity is useful, and put deterministic code on the side where correctness matters.
The boundary, in his own code
The design reads clearly in the public repository, so rather than describe it, here are the three places the boundary is actually enforced. All excerpts are from Logesh's repo, linked at each file. I am quoting the parts that make the engineering point, not reproducing his walkthrough.
1. The genome clamps whatever the model proposes
The critic model can suggest any mutation it likes. It never reaches the renderer unclamped. In
src/genome.py , every style value has a hard range, and apply_mutation forces the proposal into it before anything renders:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# name -> (default, min, max)
BOUNDS = {
"strokes": (520, 120, 1400),
"curl": (1.9, 0.3, 4.5),
"stroke_opacity": (0.34, 0.06, 0.85),
# ... every visual decision, bounded
}
def apply_mutation(genome: dict, mutation: dict) -> tuple:
"""Apply the critic's proposed deltas. Returns (new_genome, applied_changes)."""
g = sanitize(genome)
applied = {}
if isinstance(mutation, dict):
for key, value in mutation.items():
if key not in BOUNDS: # unknown key from the model: ignored
continue
before = g[key]
after = _clamp(key, value) # out-of-range value: clamped, not trusted
if abs(after - before) > 1e-9:
g[key] = after
applied[key] = {"from": round(before, 4), "to": round(after, 4)}
g["generation"] = int(g.get("generation", 0)) + 1
return g, appliedTwo things make this safe to leave running unattended. A key the model invents that is not in
BOUNDS is silently dropped. A value outside the allowed range is clamped, not accepted. The file comment says it plainly: the bounds are the whole point, which is why the agent can run for months without producing garbage. The model's creativity is welcome; its ability to break the renderer is removed.2. The fitness function, not the model, decides what "better" means
A language model that cannot see its own rendered image is a poor judge of it. So selection is a plain weighted score computed in code, in
src/fitness.py :1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
WEIGHTS = {
"coverage": 0.26, # enough ink to read as a drawing, not so much it turns to mud
"contrast": 0.20, # the sky has to separate from the land
"chroma": 0.16, # colour, not five greys
"composition": 0.14, # light source near a rule-of-thirds node
"flow": 0.16, # long sweeping strokes beat short scribbles
"novelty": 0.08, # do not repeat last week's palette
}
def evaluate(metrics: dict, palette: list, recent_palettes: list) -> dict:
parts = {
"coverage": _band(metrics.get("ink_coverage", 0.0), TARGET_COVERAGE, COVERAGE_TOLERANCE),
"contrast": _ceil(metrics.get("palette_contrast", 0.0) / TARGET_CONTRAST),
# ...
}
total = sum(WEIGHTS[k] * parts[k] for k in WEIGHTS) * 10.0
return {"fitness": round(total, 3), "craft": round(craft, 3), "parts": parts}"Better" is a number this function returns, derived from measured properties of the actual rendered image, not from the critic's opinion of it. The weights are a taste statement the author can read, tune, and defend. That is a very different thing from asking a model "is this good?" and believing the answer.
3. The orchestrator parks the critic's suggestion as a candidate, not a command
This is the line that convinced me. In
src/handler.py , the critic runs, but its mutation does not get applied. It is stored as one of four candidates that must win on measurements the next morning:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
proposals = {
"champion": {}, # yesterday's winner, unchanged
"directed": direction.get("genome_nudge") or {}, # today's weather-driven nudge
"critic": pending, # the critic's last suggestion
"explore": gen.jitter(stored, seed), # a deterministic random walk
}
# render all four, measure all four, keep the best measured one
candidates = []
for name, proposal in proposals.items():
variant, applied = gen.apply_mutation(stored, proposal)
piece = _render_capped(variant, seed)
verdict = fitness.evaluate(piece["metrics"], palette, recent_palettes)
candidates.append({"variant": name, "genome": variant, "fitness": verdict["fitness"], ...})
winner = max(candidates, key=lambda c: c["fitness"]) # the number decides, not the criticAnd later, the comment in the repository says exactly what the structure enforces:
1
2
# The critic does not get to change the style directly. Its mutation is
# parked as tomorrow's candidate and has to win on measurements.A model with a convincing explanation does not get to move the system on the strength of the explanation. It gets to enter a contest it might lose. That is the whole discipline in one data structure.
The failure that improved the design
The most valuable part of the project is not that the first version worked. It is that the first improvement loop made the output worse.
The initial approach let the critic propose a mutation and then applied that mutation to the next day's style. The critic could write a convincing explanation, but it was not directly evaluating the rendered image. Its proposal became a random walk. The system was changing because the critic had something to say, not because the result had improved.
The revised loop keeps four candidates in competition. The critic remains useful, but only as one source of exploration. Its mutation must survive measurement alongside the unchanged champion, the weather-directed variant and the deterministic exploration variant.
That is a better role for a language model. It can introduce ideas that a fixed algorithm would not invent, while the code prevents novelty from becoming an automatic promotion.
Graceful degradation is part of the creative design
The repository also documents a practical problem with the optional repaint stage. The image-model access available to the account was not dependable: one model was marked legacy and the Marketplace-backed alternatives were not available for that account.
Instead of making the entire morning depend on that entitlement, Aubade keeps its own raster path. The deterministic renderer can produce the raster plate, and the repaint is treated as an optional enhancement.
The same principle appears in the other external calls. Weather can fall back to a recorded sky, voice can fail without preventing the rest of the artefacts from being published, and the day's record keeps the errors.
An autonomous system needs a useful result when one optional dependency is unavailable. Otherwise it is not autonomous, it is a chain of hopeful calls.
What I learned from looking at this build
1. Autonomy needs an objective, not just a schedule
A scheduled Lambda function is not automatically an autonomous agent. The schedule only provides a heartbeat. Aubade gets its autonomy from the loop around that heartbeat: propose, render, measure, select and remember.
2. A critic should create hypotheses
A critic that cannot inspect the actual output should not have unilateral authority over the next state. Its suggestion is still valuable, but it belongs in the candidate pool. That makes the system curious without making it reckless.
3. Constraints make unattended generation possible
The bounded genome is not a limitation added after the creative work. It is what makes the creative work safe to leave running. The system can explore inside a known space instead of asking a model to define the limits and the result at the same time.
4. Portability includes model access
The public repository makes the model-availability issue visible. A model identifier appearing in a template does not guarantee that an account can invoke it. Bedrock access, model lifecycle and Marketplace entitlement are operational inputs, not details to discover after the deployment fails.
5. Shared challenges become more useful when the solutions differ
The strongest part of this Weekend Challenge was not that we built two versions of the same thing. We built different systems from the same constraint. My project explored autonomous information delivery. Logesh's explored autonomous creative selection. Comparing the two makes the architecture easier to reason about.
What is live and what is optional
The scheduled gallery, the deterministic composition engine, the four-candidate selection loop, the DynamoDB memory and the S3 plus CloudFront publication path are the core of the project.
The image repaint stage is optional and account-dependent. It should not be described as a required part of the live path. That distinction is important in any AWS build: planned capability and deployed capability should not share the same sentence.
Try it yourself
The repository includes a deployment script and a manual Lambda invocation path. Start with the code in this order:
- Read
src/handler.pyto see the morning loop. - Read
src/genome.pyto see how style values are constrained. - Read
src/fitness.pyto see what the system means by a better candidate. - Read
infra/template.yamlbefore deploying, especially the IAM and model configuration.
The full technical walkthrough belongs to Logesh's original Part 2. This companion post exists to close the loop promised in my Part 1 and explain why I think the model-code boundary is the design worth carrying into other agent systems.
Logesh built the system. I am sharing the engineering lesson I took from it.
Builder Log, Part 2 companion.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Tags: #aws #amazon-bedrock #aws-lambda #serverless #agentic-ai
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article