Custom Engines: Emoji Vibes & Batch BrewsΒΆ
Custom engines are where Flockβs declarative facade meets the code you actually want to write. In this tutorial you will build two wildly different EngineComponents:
- A real-time emoji interpreter that reacts to short mood updates.
- A batch-oriented potion brewer that waits for enough ingredients before revealing its recipe.
Both examples live in examples/05-engines/ so you can run them end-to-end.
1. Emoji Mood Engine (single artifact flow)ΒΆ
Goal: Turn plain-text mood updates into structured emoji summaries.
Run the example:
Key takeaways from EmojiMoodEngine:
- Inherit from
EngineComponentand overrideevaluate(). - Use
inputs.first_as(Model)to deserialize the first artifact into your Pydantic model. - Return results with
EvalResult.from_object(...); Flock turns them into blackboard artifacts automatically. - Small helper methods (_detect_mood, _explain) keep the core handler readable.
Snippet:
class EmojiMoodEngine(EngineComponent):
async def evaluate(self, agent, ctx, inputs: EvalInputs) -> EvalResult:
prompt = inputs.first_as(MoodPrompt)
if not prompt:
return EvalResult.empty()
detected, keywords = self._detect_mood(prompt.message)
emoji = self.MOOD_EMOJI.get(detected, self.MOOD_EMOJI["curious"])
artifact = MoodEmoji(
speaker=prompt.speaker,
detected_mood=detected,
emoji=emoji,
explanation=self._explain(prompt.message, detected, keywords),
)
return EvalResult.from_object(artifact, agent=agent)
Try it yourself:
- Add the word βnapβ to a message and see which emoji appears.
- Extend
MOOD_KEYWORDSto include your teamβs inside jokes. - Swap
EvalResult.from_objectforEvalResult.from_objectsif you want multiple emoji outputs.
2. Potion Batch Engine (BatchSpec showcase)ΒΆ
Goal: Gather three whimsical ingredients, then publish a fully-fledged potion recipe.
Run the example:
Highlights from PotionBatchEngine:
- The agent subscribes with
.consumes(PotionIngredient, batch=BatchSpec(size=3)). evaluate_batch()receives all accumulated artifacts at once.inputs.all_as(Model)deserializes every ingredient payload for easy iteration.- You can still keep a fallback
evaluate()if something calls the engine outside of batching.
Snippet:
class PotionBatchEngine(EngineComponent):
async def evaluate_batch(self, agent, ctx, inputs: EvalInputs) -> EvalResult:
ingredients = inputs.all_as(PotionIngredient)
if not ingredients:
return EvalResult.empty()
recipe = PotionRecipe(
title=self._name_potion(ingredients),
incantation=self._craft_incantation(ingredients),
tasting_notes=self._describe_feel(ingredients),
ingredients=[f"{item.name} ({item.effect})" for item in ingredients],
)
return EvalResult.from_object(recipe, agent=agent)
Try it yourself:
- Change
BatchSpec(size=3)toBatchSpec(size=2, timeout=timedelta(seconds=3))to see timeout-driven flushes. - Add a fourth ingredient after a flush to confirm the accumulator resets cleanly.
- Return two artifacts (e.g.,
PotionRecipeandSafetyInstructions) viaEvalResult.from_objects.
When to build a custom engineΒΆ
- You want deterministic logic written in Python (rule-based, API calls, DSPy overrides).
- You need batching behaviour that standard engines do not provide out of the box.
- You want to reuse a βlibraryβ of engines across multiple agents.
Pair these engines with tracing (FLOCK_AUTO_TRACE=true) to watch every decision step in DuckDB.
Next: bring similar creativity to agent components with Custom Agent Components.