Sitemap

Building self-evolving AI systems: exploring the architecture

27 min readJan 27, 2026

--

Press enter or click to view image in full size

Have you ever wanted to leave your AI coding assistant a list of tasks overnight — and wake up to finished pull requests?

Not one task. Ten. Twenty. Running in parallel while you sleep.

I think about this constantly. Every time I close a Claude Code session, every time I commit and start a new task from scratch, I wonder:

Why am I still the bottleneck?

The AI can write code. Often better and faster than I can. But I’m still here, watching, waiting, context-switching between tasks one at a time.

What if I could just… leave? Define what needs to be done, walk away, and come back to results?

What about existing platforms?

There are already services that promise autonomous AI development. GitHub Copilot has Agent Mode. CTO.new offers automated coding (and it’s currently free). Various startups are building similar tools.

I’ve tried several of them. They work — for certain tasks. Simple features, greenfield projects, well-defined problems. They can be genuinely useful.

But here’s what I found: complex tasks don’t go well. And starting a serious project from scratch — with your own conventions, your own architecture, your own rules — doesn’t really work either. You end up adapting to their system instead of the system adapting to you.

And everything has its price.

When you use a third-party AI development service, your code travels to their servers. Your proprietary logic. Your business rules. Your competitive advantage. Processed somewhere you don’t control.

For personal projects, that might be fine. But if you’re developing internal services for a company….

For some teams, the tradeoffs are acceptable. For others — especially in regulated industries, enterprise environments, or with sensitive IP — they’re not. They need a system that runs on their infrastructure, integrates with their internal tools, and keeps code inside their walls.

This article is for those who want to build their own system.

Not because existing platforms are bad — they have their place. But because I wanted something different: a system that solves my problems the way I want them solved. A system I own, I control, and I can shape to fit my specific needs.

The idea that captured my imagination

What if the system could extend itself?

Think about it.

You have a development system. It can clone repos, run AI agents, create pull requests. But then you need Slack notifications. In a traditional setup, you’d stop everything, build a Slack integration, deploy it, wire it up.

What if instead, the system recognized it needed Slack capabilities — and built them itself?

  • Need to parse PDFs? It creates a PDF parser service.
  • Need semantic search? It adds embeddings and vector storage.
  • Need to interact with your internal CRM? It builds an adapter.

Each new capability becomes a building block for future tasks. The system grows not because you’re constantly building it, but because it builds itself to meet your needs.

The core principle: AI doesn’t write code from scratch — AI combines capabilities from a registry.

The system maintains a catalog of what it can do.

  • Send a message.
  • Clone a repository.
  • Generate embeddings.
  • Run a code review.

Each capability has a simple contract: here’s the input, here’s the output. AI looks at this registry, sees what’s available, and composes solutions from existing blocks.

If a capability is missing? AI creates it, adds it to the registry, and now it’s available for future use.

Let me show you what this looks like in practice.

Imagine a document processing workflow — customers submit applications, and the system needs to analyze them and notify the team.

The initial workflow

Press enter or click to view image in full size

The workflow is simple, but limited. It only handles text files. Nobody gets notified. The team misses urgent applications.

AI identifies missing capabilities

When asked to improve the workflow, AI looks at the registry:

Press enter or click to view image in full size

AI creates new microservices

Each new service is small, focused, with a clear contract:

// pdf-parser service (~200 lines of code)
// Contract — everything other services need to know
type ParsePDFInput struct {
Content []byte `json:"content"`
}

type ParsePDFOutput struct {
Text string `json:"text"`
PageCount int `json:"page_count"`
Tables []Table `json:"tables"`
}

// slack-client service (~150 lines of code)
type SendMessageInput struct {
Channel string `json:"channel"`
Text string `json:"text"`
}

type SendMessageOutput struct {
MessageID string `json:"message_id"`
SentAt time.Time `json:"sent_at"`
}

AI writes the implementation, runs self-review, creates infrastructure configuration, deploys. Both services register their capabilities. Now the registry has two new entries.

Press enter or click to view image in full size

The updated workflow code

func ProcessDocumentWorkflow(ctx workflow.Context, doc Document) error {
// Save original to S3
var stored UploadFileOutput
workflow.ExecuteActivity(ctx, "UploadFile", UploadFileInput{
Content: doc.RawContent,
Destination: fmt.Sprintf("s3://documents/incoming/%s", doc.Name),
}).Get(ctx, &stored)

// Extract text based on format
var textPath string

if doc.Type == "pdf" {
// 🆕 New capability: PDF parsing
// Pass S3 path, not content — PDFs can be 10-20 MB
var parsed ParsePDFOutput
workflow.ExecuteActivity(ctx, "ParsePDF", ParsePDFInput{
SourcePath: stored.S3Path,
}).Get(ctx, &parsed)
textPath = parsed.TextPath
} else {
textPath = stored.S3Path // Text files don't need parsing
}

// Read extracted text for AI analysis
var textContent ReadFileOutput
workflow.ExecuteActivity(ctx, "ReadFile", ReadFileInput{
Path: textPath,
}).Get(ctx, &textContent)

// AI analyzes
var analysis ChatSessionOutput
workflow.ExecuteActivity(ctx, "ChatSession", ChatSessionInput{
SystemPrompt: "Analyze this customer application...",
Messages: []Message{{Role: "user", Content: textContent.Content}},
}).Get(ctx, &analysis)

// Parallel: save to database AND notify team
workflow.Go(ctx, func(ctx workflow.Context) {
workflow.ExecuteActivity(ctx, "SaveRecord", SaveRecordInput{
Type: "application",
Content: analysis.Response,
}).Get(ctx, nil)
})

workflow.Go(ctx, func(ctx workflow.Context) {
// 🆕 New capability: Slack notifications
workflow.ExecuteActivity(ctx, "SendMessage", SendMessageInput{
Channel: "#new-applications",
Text: fmt.Sprintf("New application: %s\n\nSummary: %s",
doc.Name, analysis.Summary),
}).Get(ctx, nil)
})

return nil
}

What just happened?

The system identified what it couldn’t do, created the missing pieces, and composed them into an improved workflow. The new services — pdf-parser and slack-client — are now available for any future workflow. Need PDF parsing somewhere else? It's already in the registry. Need Slack notifications for a different process? Same service, different workflow.

A system tailored to your business. Running on your infrastructure. Evolving to match your requirements. Without your code ever leaving your control.

This is the pattern I’ve been exploring. How would such a system actually work? What architecture would it need? What tools would help?

Let’s work through this together.

Looking at that example, we can extract the requirements:

Parallel execution. Not one agent working through a queue — multiple agents, each with their own isolated workspace, working simultaneously. Ten tasks? Ten agents. Each independent.

Fault tolerance. If one task fails, the others continue. If a service crashes, running workflows don’t die — they wait for recovery. No cascading failures.

Coordination. Something has to route tasks to the right workers, track progress, handle retries. Not manual orchestration — automated routing.

Self-documentation. When AI operates alone, anything not written down is lost forever. The system must document every decision, every change, every reasoning. This becomes the system’s memory.

Composability. New capabilities should plug in without rewiring the whole system. Add a service, it becomes available to all workflows.

Registry of capabilities. AI needs to know what it can do. A catalog of available services, their inputs and outputs. This could be markdown documentation, a JSON schema, a dedicated service — the format matters less than the principle: AI sees the menu of possibilities, not the kitchen.

This is a lot. But there’s an architecture pattern that addresses all of it — and it’s simpler than you might expect.

When the human leaves the loop, everything changes

Manual AI-assisted development and automated AI-powered systems are fundamentally different.

When you work with Claude Code or Cursor today, you’re in the loop. You watch the AI work. You catch mistakes. You hold context in your head. You make decisions. One task at a time.

An automated system is different. You’re not the operator — you’re the reviewer. You set tasks. The system handles execution: cloning, developing, testing, reviewing its own code, creating pull requests. Multiple tasks in parallel. Accessible from anywhere.

Press enter or click to view image in full size

When you remove the human from the execution loop, the system must compensate.

A developer often doesn’t document their decisions. They forget things. They make changes without explaining why. That’s fine — the context lives in their head.

When AI handles development autonomously, it has no persistent memory between sessions. If it doesn’t document why it made a decision, that context is lost forever. The next AI session won’t know. The next human reviewer won’t know.

This is why the architecture needs to capture everything: what happened, why it happened, what was considered and rejected. Not as an afterthought — as a core design principle.

The architecture is built on just two concepts

Activity — a capability in the registry. A function that takes input, does something, returns output. That’s it. No HTTP layers, no repository patterns. Just: “I can do this thing. Here’s what I need, here’s what I’ll give back.”

Workflow — a composition of Activities. First do this, then that, handle errors this way, retry that way. Pure orchestration.

A workflow is just code.

It’s a service written in any language that has a Temporal SDK — Go, PHP, Python, TypeScript, Java. You describe the sequence of activities using normal programming constructs: conditionals, loops, parallel execution, timers. The service registers with Temporal (our orchestrator — more on this shortly), and you can invoke it by name.

// A workflow is just a function that orchestrates activities
// (This uses Temporal SDK syntax — we'll explain Temporal in the next section)
func ProcessOrderWorkflow(ctx workflow.Context, order Order) error {
// Sequential: validate, then charge
workflow.ExecuteActivity(ctx, "ValidateOrder", order).Get(ctx, nil)
workflow.ExecuteActivity(ctx, "ChargePayment", order.Payment).Get(ctx, nil)

// Parallel: notify customer AND update inventory simultaneously
workflow.Go(ctx, func(ctx workflow.Context) {
workflow.ExecuteActivity(ctx, "SendConfirmation", order.Email).Get(ctx, nil)
})
workflow.Go(ctx, func(ctx workflow.Context) {
workflow.ExecuteActivity(ctx, "UpdateInventory", order.Items).Get(ctx, nil)
})

// Wait with timeout
workflow.Sleep(ctx, 24*time.Hour)
workflow.ExecuteActivity(ctx, "SendFollowUp", order.Email).Get(ctx, nil)

return nil
}

When AI creates or modifies a workflow, it’s writing code. It creates a new service, or updates an existing one, then deploys it. The workflow becomes available to call by name. This is why the system can evolve — AI isn’t configuring a visual tool, it’s writing and deploying actual code.

How AI sees the system

┌─────────────────────────────────────────────────────────────────┐
│ How AI Sees the System │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Registry (what AI looks at): │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ telegram-client: │ │
│ │ • SendMessage(chat_id, text) → (message_id) │ │
│ │ • SendDocument(chat_id, file) → (file_id) │ │
│ │ │ │
│ │ git-workspace: │ │
│ │ • SpawnWorkspace(repo_url) → (workspace_id) │ │
│ │ • ReadFile(path) → (content) │ │
│ │ • WriteFile(path, content) → (success) │ │
│ │ │ │
│ │ agent: │ │
│ │ • ChatSession(prompt, messages) → (response) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ AI thinks: "I need to review code. Let me combine: │
│ SpawnWorkspace → ReadFile → ChatSession → SendMessage" │
│ │
└─────────────────────────────────────────────────────────────────┘

Compare this to a typical monolith where AI must navigate HTTP layers, controllers, services, repositories, domain models, database abstractions…

  • Too many decisions.
  • Too many places to make mistakes.
  • Too much context to hold.

The separation of complexity

Here’s why this works so well for AI:

Activity services contain the real logic. How does Telegram API work? How do you handle rate limits? What’s the retry strategy? All of that lives inside the activity service. Complex, tested, hidden behind a simple contract.

Workflow services only orchestrate. They call activities in sequence, handle branching, manage errors. That’s it. A typical workflow service is 100–300 lines of code. No business logic — just composition.

┌─────────────────────────────────────────────────────────────────┐
│ Separation of Complexity │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Activity Services │ Workflow Services │
│ (where complexity lives) │ (pure orchestration) │
│ ───────────────────────── │ ───────────────────── │
│ • Real logic inside │ • Only calls activities │
│ • Hidden behind contracts │ • ~100-300 lines │
│ • AI doesn't need to know │ • AI easily understands │
│ • Reused by many workflows │ • AI easily modifies │
│ │ │
└─────────────────────────────────────────────────────────────────┘

This separation is why AI can create and modify workflows effectively. The workflow code is simple — it just orchestrates. The complex stuff is hidden away in activity services that AI doesn’t need to touch.

The key insight: AI works with the registry, not the codebase.

// This is what AI sees in the registry
// Internal implementation is irrelevant
type SendMessageInput struct {
ChatID int64 `json:"chat_id"`
Text string `json:"text"`
ParseMode string `json:"parse_mode"`
}

type SendMessageOutput struct {
MessageID int64 `json:"message_id"`
SentAt time.Time `json:"sent_at"`
}

When AI looks at this contract, it knows exactly what it can do.

  • Send a message.
  • Get back a message ID.

Done.

It doesn’t need to know how the Telegram API works internally, what HTTP library is used, how errors are handled, or what retry logic exists. The contract is the interface. Everything else is hidden.

Why Temporal as the orchestrator

I’ve thought about different approaches. Message queues. Custom orchestration. Cron jobs with state machines. They work for simple cases and tend to fall apart at scale.

Temporal stands out. Not just because it’s battle-tested (Uber, Netflix, Snap use it), but because of a fundamental design decision:

Services don’t call each other. Temporal routes everything.

┌─────────────────────────────────────────────────────────────┐
│ TEMPORAL │
│ (Orchestration Layer) │
│ │
│ Workflow says: │
│ "Execute SendMessage on queue 'telegram-client'" │
│ │
│ Temporal finds the right worker │
│ Routes the task │
│ Waits for response │
│ Handles retries if needed │
│ │
└─────────────┬─────────────────┬─────────────────┬───────────┘
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│telegram- │ │embeddings│ │ git- │
│ client │ │ │ │workspace │
└──────────┘ └──────────┘ └──────────┘

Each service connects to Temporal and registers its capabilities in a task queue (Like channels). When a workflow needs something done, it doesn’t call the service directly — it tells Temporal “here’s a task, find someone who can do it.”

If no worker is available? The workflow waits. It doesn’t crash. It doesn’t timeout prematurely. It waits until someone can handle the task.

This changes everything for self-evolving systems.

The self-healing scenario

Imagine this: a workflow is running, it calls an Activity, and the service has a bug. The Activity fails.

In a traditional system, you’d have cascading failures. Timeouts. Retries that make things worse. Manual intervention.

With Temporal:

1. Workflow running, calls "ProcessData" activity


2. Service has bug → Activity fails
(Workflow WAITS, doesn't crash)


3. AI sees error in Temporal logs/UI


4. AI analyzes, fixes code, commits to git


5. AI rebuilds and restarts service in Kubernetes


6. Service reconnects to Temporal


7. Temporal automatically retries the failed activity


8. Activity succeeds, workflow continues

Total workflow downtime: ZERO (it was waiting)

The AI can fix its own mistakes without breaking running workflows. This is essential for self-evolving systems — it provides the fault tolerance that makes autonomous operation possible.

What Temporal provides out of the box

  • Durability: Workflows survive service restarts, even server restarts
  • Retry policies: Built-in, configurable, no custom code needed
  • Language agnostic: Write services in Go, PHP, Python, Node — all talk to the same Temporal
  • Universal serialization: Data serialization/deserialization handled automatically
  • Observability: See every step of every workflow (more on this shortly)

And here’s something worth noting:

AI writes Temporal code well.

The structure is predictable. Input, output, Activity, Workflow. There’s not much room to get creative — and that’s a feature.

Signals: Human-in-the-loop when needed

A workflow doesn’t have to be fully autonomous. It can pause and wait for external input. This opens up an interesting possibility: interactive AI sessions.

Imagine a workflow working on a complex task. The AI encounters an ambiguity — maybe it’s unclear which approach to take, or it needs clarification on requirements. Instead of guessing (and potentially wasting time), the workflow can:

  1. Send you a message (via Slack, Telegram, or email)
  2. Pause and wait for your response
  3. Receive your input as a signal
  4. Continue with the new context
┌─────────────────────────────────────────────────────────────────┐
│ Interactive Workflow with Signals │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Workflow starts task │
│ │ │
│ ▼ │
│ AI encounters ambiguity │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ Sends question to your chat: │ │
│ │ "Should I use REST or GraphQL │ │
│ │ for this API? Here's my analysis: │ │
│ │ ..." │ │
│ └─────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Workflow WAITS (could be minutes, hours, days) │
│ │ │
│ │◄─────── You reply: "Use GraphQL, and also │
│ │ consider adding a caching layer" │
│ │ │
│ ▼ │
│ Signal received → Workflow continues with new context │
│ │ │
│ ▼ │
│ Task completed with human guidance │
│ │
└─────────────────────────────────────────────────────────────────┘

This isn’t a chat with a stateless AI. The workflow maintains full context of everything it has done. When you respond, the AI has the complete history — what it tried, what it considered, what it was stuck on.

You get the best of both worlds: autonomous execution when things are clear, human guidance when it matters.

Let’s see what these services actually look like.

Every microservice in this architecture follows the same structure:

┌─────────────────────────────────────────────┐
│ MICROSERVICE │
├─────────────────────────────────────────────┤
│ │
│ Name: telegram-client │
│ Task Queue: telegram-client │
│ Responsibility: Send messages to Telegram │
│ │
│ Activities: │
│ • SendMessage │
│ • SendDocument │
│ │
│ Each activity: ~50-1000 lines │
│ Total service: ~3000 lines │
│ │
└─────────────────────────────────────────────┘

One service. One responsibility. A handful of Activities. That’s it.

Example services

Here’s what a library of building blocks might look like:

telegram-client — Send messages to Telegram

  • SendMessage: Send text to a chat
  • SendDocument: Send files and attachments

embeddings — Text vectorization for semantic search

  • ChunkText: Split documents into smaller pieces
  • GenerateEmbedding: Convert text to vectors
  • ChunkAndEmbed: Do both in one call

git-workspace — Repository operations

  • SpawnWorkspace: Clone a repo into isolated environment
  • ReadFile, WriteFile, etc: File operations
  • GitCommit, GitPush, etc: Version control

vector-db — Semantic search storage

  • CreateCollection: Set up a new index
  • UpsertDocuments: Add or update vectors
  • SearchByVector: Find similar content

agent — AI orchestration

  • ChatSession: Run a conversation with context
  • CallOpenAI: Direct API calls

scheduler — Task scheduling

  • CreateSchedule: Set up recurring workflows
  • PauseSchedule, TriggerSchedule: Control execution

Each of these services could be written by AI in roughly an hour. Clear feature request, clear contracts, focused scope.

Why small services work with AI

Here’s the pattern that makes this practical:

AI writes code


AI runs code review (iteration 1)


Issues found? ──YES──▶ AI fixes, review again

NO


Code ready for PR

This loop might run 5–10 times. On a 3000-line microservice, that’s fast — maybe 30 minutes total. On a 100,000-line monolith, it becomes impractical.

Small services mean:

  • Fast review cycles
  • Fewer places where things can break
  • AI holds entire architecture in context
  • Mistakes are isolated, not cascading

Universal building blocks

Notice something about these services? They’re not tied to any specific business.

Sending a Telegram message. Reading a file from git. Generating embeddings. These are universal operations.

You could take these exact services, deploy them to your infrastructure, and build completely different workflows on top. The building blocks are reusable.

Full transparency: The Temporal UI

When AI operates autonomously, you need visibility into every decision. You can’t watch AI work in real-time, but you can see exactly what happened after the fact. Temporal UI provides this out of the box.

Workflow List

Press enter or click to view image in full size

Every workflow execution is tracked. You see what’s running, what completed, what failed. Filter by workflow type, status, time range. When you have dozens of AI agents working in parallel, this becomes your control center.

Execution Timeline

Press enter or click to view image in full size

Each workflow shows its complete execution path. Which activities ran, in what order, how long each took. The visual timeline makes it obvious where time was spent — and where things went wrong.

Press enter or click to view image in full size

Workflow Hierarchies

Complex workflows spawn child workflows. The UI shows the complete tree — which parent started which children, how they relate, what state each is in.

Failed Activities and Retries

When an activity fails, you see every retry attempt. What error occurred, when it was retried, whether it eventually succeeded. This visibility is essential for understanding how the system behaves under failure conditions.

Press enter or click to view image in full size

This is event sourcing built in. Every state change, every decision, every retry — recorded and queryable. When AI makes mistakes, you can trace exactly what happened. When you need to understand why the system did something, the answer is always there.

Infrastructure as Code: The AI’s map

For AI to create and deploy new services autonomously, it needs to understand not just the code, but the infrastructure. What services exist? How are they configured? How do you add a new one?

This is where Infrastructure as Code becomes essential.

What is Infrastructure as Code?

Instead of manually configuring servers, databases, and services through dashboards and CLI commands, you describe your infrastructure in code files. These files become the single source of truth for your system.

Terraform is one of the most popular tools for this. You write .tf files that describe what you want:

# This file describes a microservice deployment
# AI reads this and understands the entire system
module "telegram_client" {
source = "./modules/microservice"
name = "telegram-client"
image = "registry/telegram-client:latest"
task_queue = "telegram-client"
replicas = 2
env_vars = {
TEMPORAL_ADDRESS = "temporal:7233"
TELEGRAM_TOKEN = var.telegram_token
}
}

module "embeddings" {
source = "./modules/microservice"
name = "embeddings"
image = "registry/embeddings:latest"
task_queue = "embeddings"
replicas = 3
}

Then you run terraform apply, and Terraform makes reality match your description. It creates what's missing, updates what's changed, removes what's no longer defined.

Why this matters for AI?

When AI reads Terraform files, it sees:

  • What services exist
  • How they’re configured
  • What resources they need
  • How to add new services (just follow the pattern)

The pattern for adding a new service becomes mechanical:

1. AI identifies need for new capability


2. AI creates new microservice (code)


3. AI creates Terraform module (infrastructure)


4. AI runs terraform plan (validation)


5. AI applies changes to Kubernetes


6. New service connects to Temporal


7. Capability available to all workflows

The system grows itself. And because everything is code — services, infrastructure, configuration — it’s all documented. AI can trace back any decision by looking at git history.

Kubernetes: The runtime environment

Terraform describes what you want. Kubernetes runs it.

Kubernetes is a container orchestration platform. You tell it “run 3 copies of this service,” and it figures out where to put them, restarts them if they crash, scales them up or down based on load.

For our purposes, Kubernetes provides:

  • Isolation: Each service runs in its own container
  • Scaling: Need more capacity? Add replicas
  • Self-healing: Container dies? Kubernetes restarts it
  • Networking: Services can find each other by name

The combination of Terraform (describing infrastructure) and Kubernetes (running it) gives AI everything it needs to manage the system autonomously.

Connecting existing systems

Temporal isn’t limited to services running inside Kubernetes.

You can expose Temporal outside the cluster through an Ingress. This means your existing systems — monoliths, legacy applications, services running on VMs — can connect to the same Temporal instance and use the same Activities and Workflows.

┌─────────────────────────────────────────────────────────────────┐
│ Temporal as a Bridge │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Kubernetes Cluster │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ pdf- │ │embeddings│ │ git- │ │ │
│ │ │ parser │ │ │ │workspace │ │ │
│ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │
│ │ │ │ │ │ │
│ │ └─────────────┼─────────────┘ │ │
│ │ ▼ │ │
│ │ ┌──────────────┐ │ │
│ │ │ TEMPORAL │◄─── Ingress ◄────────┐ │ │
│ │ └──────────────┘ │ │ │
│ │ ▲ │ │ │
│ └──────────────────────┼──────────────────────────────┼───┘ │
│ │ │ │
│ ┌──────────────────────┼──────────────────────────────┼───┐ │
│ │ │ Outside Cluster │ │ │
│ │ ┌──────────────────┴───────────┐ │ │ │
│ │ │ │ │ │ │
│ │ │ Your Existing Monolith │──────────────────┘ │ │
│ │ │ (PHP, Java, .NET...) │ │ │
│ │ │ │ │ │
│ │ │ • Customer registration │ │ │
│ │ │ • Order processing │ │ │
│ │ │ • Payment workflows │ │ │
│ │ │ │ │ │
│ │ └──────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘

Why does this matter?

You don’t have to rewrite everything. Your monolith can:

  • Register its own Activities (customer validation, payment processing, inventory checks)
  • Compose Workflows using both its own Activities AND microservice Activities
  • Gradually migrate business logic to smaller services over time

Example: Customer registration workflow

Your monolith handles customer registration. It’s complex — validation, fraud checks, CRM updates, welcome emails. You can orchestrate this as a Temporal Workflow:

func CustomerRegistrationWorkflow(ctx workflow.Context, customer Customer) error {
// Activity from your monolith
workflow.ExecuteActivity(ctx, "ValidateCustomer", customer).Get(ctx, nil)

// Activity from your monolith
workflow.ExecuteActivity(ctx, "CreateCRMRecord", customer).Get(ctx, nil)

// Activity from microservice (embeddings for semantic search)
workflow.ExecuteActivity(ctx, "IndexCustomerProfile", customer).Get(ctx, nil)

// Activity from microservice (notifications)
workflow.ExecuteActivity(ctx, "SendWelcomeEmail", customer.Email).Get(ctx, nil)

// Activity from microservice (Slack notification to sales team)
workflow.ExecuteActivity(ctx, "SendMessage", SendMessageInput{
Channel: "#new-customers",
Text: fmt.Sprintf("New customer: %s", customer.Name),
}).Get(ctx, nil)

return nil
}

The workflow mixes Activities from your monolith with Activities from microservices. Temporal routes each task to the right worker — it doesn’t care where the service runs.

This architecture isn’t just for AI development. It’s for orchestrating any business process. The AI development use case is one application. But the same patterns work for:

  • Order fulfillment pipelines
  • Document approval workflows
  • Data processing pipelines
  • Integration with third-party services
  • Anything that involves coordinating multiple steps

Now let’s see how AI actually composes workflows from the registry.

When AI receives a task — say, “set up automated code review” — it follows a predictable process:

┌─────────────────────────────────────────────────────────────────┐
│ AI's Composition Process │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. LOOK AT REGISTRY │
│ "What capabilities do I have?" │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ git-workspace: SpawnWorkspace, ReadFile, WriteFile │ │
│ │ agent: ChatSession, CallOpenAI │ │
│ │ github: PostComment, CreatePR, GetChangedFiles │ │
│ │ telegram: SendMessage, SendDocument │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 2. THINK HOW TO COMBINE │
│ "For code review, I need to:" │
│ • Get the code → SpawnWorkspace + ReadFile │
│ • Review it → ChatSession │
│ • Post feedback → PostComment │
│ │ │
│ ▼ │
│ 3. COMPOSE WORKFLOW │
│ SpawnWorkspace → ReadFile → ChatSession → PostComment │
│ │ │
│ ▼ │
│ 4. MISSING SOMETHING? │
│ "I need to notify the author, but no Slack service..." │
│ → Propose new service → Strategy session with human │
│ │
└─────────────────────────────────────────────────────────────────┘

Example: Automated code review

func CodeReviewWorkflow(ctx workflow.Context, repoURL string, prNumber int) error {

// 1. Create isolated workspace for the repository
var workspace SpawnWorkspaceOutput
workflow.ExecuteActivity(ctx, "SpawnWorkspace", SpawnWorkspaceInput{
RepoURL: repoURL,
Branch: fmt.Sprintf("refs/pull/%d/head", prNumber),
}).Get(ctx, &workspace)

// 2. Read the changed files
var files ReadFilesOutput
workflow.ExecuteActivity(ctx, "ReadFiles", ReadFilesInput{
Paths: getChangedPaths(prNumber),
}).Get(ctx, &files)

// 3. AI reviews the code
var review ChatSessionOutput
workflow.ExecuteActivity(ctx, "ChatSession", ChatSessionInput{
SystemPrompt: "You are a senior code reviewer...",
Messages: []Message{{
Role: "user",
Content: fmt.Sprintf("Review this code:\n%s", files.Content),
}},
}).Get(ctx, &review)

// 4. Post review as PR comment
workflow.ExecuteActivity(ctx, "PostPRComment", PostCommentInput{
Repository: repoURL,
PRNumber: prNumber,
Body: review.Response,
}).Get(ctx, nil)

// 5. Cleanup
workflow.ExecuteActivity(ctx, "DestroyWorkspace", DestroyWorkspaceInput{
WorkspaceID: workspace.WorkspaceID,
}).Get(ctx, nil)

return nil
}

Look at what AI doesn’t need to know:

  • How git cloning works internally
  • How the AI model API is called
  • How GitHub comments are posted
  • How workspace cleanup happens

It only knows: SpawnWorkspace takes this input and returns that output. Contract in, contract out.

Building RAG the same way

Want semantic search? Same pattern:

func IndexDocumentWorkflow(ctx workflow.Context, docPath string) error {

// Read document
var content ReadFileOutput
workflow.ExecuteActivity(ctx, "ReadFile", ReadFileInput{
Path: docPath,
}).Get(ctx, &content)

// Chunk and generate embeddings
var chunks ChunkAndEmbedOutput
workflow.ExecuteActivity(ctx, "ChunkAndEmbed", ChunkAndEmbedInput{
Text: content.Content,
ChunkSize: 5000,
}).Get(ctx, &chunks)

// Store in vector database
workflow.ExecuteActivity(ctx, "UpsertDocuments", UpsertInput{
Collection: "knowledge-base",
Documents: toVectorDocs(chunks),
}).Get(ctx, nil)

return nil
}

Three services. Three Activities. RAG pipeline done.

This is what I mean by “one architecture, many capabilities.” The same approach gives you:

  • Code review automation
  • RAG / semantic search
  • Notification systems
  • Scheduled tasks
  • Whatever else you compose

Now let’s put it all together. This is the complete cycle of how a self-evolving system works:

┌─────────────────────────────────────────────────────────────┐
│ The Self-Evolving Loop │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. Need identified │
│ "We need to send notifications to Slack" │
│ │ │
│ ▼ │
│ 2. Feature request created │
│ Clear contract defined │
│ │ │
│ ▼ │
│ 3. AI develops microservice (~1 hour) │
│ Writes code, tests, documentation │
│ │ │
│ ▼ │
│ 4. Iterative code review (5-10 cycles) │
│ AI reviews own code until no issues │
│ │ │
│ ▼ │
│ 5. AI creates Terraform module │
│ Infrastructure as code │
│ │ │
│ ▼ │
│ 6. Deploy to Kubernetes │
│ Service connects to Temporal │
│ │ │
│ ▼ │
│ 7. New capability available │
│ All workflows can now use Slack notifications │
│ │ │
│ ▼ │
│ 8. AI documents the addition │
│ Updates system knowledge base │
│ │ │
│ └─────────────▶ Loop continues │
│ │
└─────────────────────────────────────────────────────────────┘

The system grows. Each new service becomes a building block for future workflows.

Self-documentation is not optional

When AI operates autonomously, it must document everything:

  • What was built
  • Why it was built (the original need)
  • How it works (contracts, not internals)
  • What was considered and rejected

This could be a dedicated Documents service:

type CreateDocumentInput struct {
Type string // "decision", "architecture", "task", "knowledge"
Title string
Content string
Related []string // IDs of related documents
}

Without this, the system becomes a black box. With it, any AI session can understand the full context of why the system looks the way it does.

Not just new services — better workflows

The system doesn’t just add new capabilities. It can improve how it works.

You watch AI execute a workflow. Something feels off — maybe it’s asking unnecessary questions, or not asking when it should, or missing a validation step. You give feedback: “Add a check here” or “Ask me before proceeding with this part.”

AI proposes changes to the workflow. You agree. AI knows which service contains that workflow — because workflows are just services too — and modifies the code.

┌─────────────────────────────────────────────────────────────────┐
│ Feedback Loop: Improving Workflows │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. AI executes workflow │
│ │ │
│ ▼ │
│ 2. You observe the result │
│ │ │
│ ▼ │
│ 3. Give feedback: │
│ "Add validation here" │
│ "Ask me before this step" │
│ "This check is unnecessary" │
│ │ │
│ ▼ │
│ 4. AI proposes workflow changes │
│ │ │
│ ▼ │
│ 5. You agree │
│ │ │
│ ▼ │
│ 6. AI modifies the workflow service │
│ (knows where the code lives) │
│ │ │
│ ▼ │
│ 7. Workflow improved │
│ │
└─────────────────────────────────────────────────────────────────┘

This works because workflow services are simple. They don’t contain complex logic — they orchestrate. Call this activity, then that one, handle errors this way. Maybe 100–300 lines of code. Easy for AI to understand and modify.

Everything is a workflow

You can have different workflows for different purposes:

  • workflow-code-review — how AI reviews pull requests
  • workflow-create-service — how AI builds new microservices
  • workflow-feature-development — how AI implements new features
  • workflow-refactoring — how AI handles code improvements

Each can be configured differently:

  • Where AI works autonomously
  • Where it pauses for human input
  • What checks it runs
  • Who it notifies

And they evolve. You start simple. Add steps as you learn what’s needed. Remove what’s unnecessary. Adapt to your team’s preferences.

There’s no single “right” way. You design the workflows that fit your context. And they grow with you.

The human role shifts

In a self-evolving system, humans don’t write boilerplate. They:

  • Define what the system should become
  • Review pull requests
  • Make architectural decisions
  • Handle edge cases AI can’t

You become the architect and reviewer, not the coder. The system does the implementation.

Scaling up: The AI development team

Once you have the basic architecture working, you can think bigger. What if you wanted your own AI development team?

Imagine: a Business Analyst agent that takes a vague request and turns it into a structured feature specification. A Product Owner agent that breaks it into tasks and prioritizes. Backend and Frontend Developer agents that implement. A QA agent that writes and runs tests. A Code Reviewer agent that checks everything before it’s merged.

┌─────────────────────────────────────────────────────────────────┐
│ AI Development Team │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Feature Request │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Business │ Analyzes request, clarifies requirements │
│ │ Analyst │ Creates structured specification │
│ └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Product │ Breaks into tasks, sets priorities │
│ │ Owner │ Creates work plan │
│ └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────┐ │
│ │ Developers (parallel) │ │
│ │ ┌─────────┐ ┌─────────┐ │ │
│ │ │ Backend │ │Frontend │ │ Work simultaneously │
│ │ └────┬────┘ └────┬────┘ │ │
│ └───────┼──────────────┼───────────┘ │
│ │ │ │
│ └──────┬───────┘ │
│ ▼ │
│ ┌──────────────┐ │
│ │ QA │ Writes tests, runs validation │
│ │ Engineer │ Reports issues back to devs │
│ └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Code │ Reviews all changes │
│ │ Reviewer │ Approves or requests fixes │
│ └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Technical │ Updates docs, writes changelogs │
│ │ Writer │ Documents new APIs and features │
│ └──────┬───────┘ │
│ │ │
│ ▼ │
│ Pull Request ready for human review │
│ │
└─────────────────────────────────────────────────────────────────┘

This isn’t magic. Each “agent” is just a workflow that calls the right activities with the right prompts. The Business Analyst is a ChatSession with a specific system prompt. The Developer is a combination of git-workspace activities and code generation. The QA Engineer runs tests and reports results.

The complexity is in the orchestration, not in the agents themselves. And orchestration is exactly what this architecture handles.

Here’s what I think could accelerate this for everyone.

Imagine a GitHub organization — a shared repository of microservices designed for AI-assisted development. Building blocks that anyone can use.

Plug-and-play services

Each repository contains:

  • Service code — clear contracts, one responsibility
  • Terraform module — ready to plug into your infrastructure
  • Kubernetes manifests — YAML configs for direct deployment
  • Documentation — contracts, usage examples, configuration options
github.com/self-evolving-systems/
├── telegram-client/
│ ├── src/ # Service code (Go/PHP/Python)
│ ├── terraform/ # Terraform module
│ ├── k8s/ # Kubernetes manifests
│ └── README.md # Contracts & usage
├── embeddings/
├── git-workspace/
├── vector-db/
├── scheduler/
└── ...

How it works

You want to add Telegram notifications to your system:

# Option 1: Terraform
module "telegram" {
source = "github.com/self-evolving-systems/telegram-client//terraform"

telegram_token = var.telegram_token
temporal_address = var.temporal_address
}

# Option 2: Direct Kubernetes
kubectl apply -f https://raw.githubusercontent.com/self-evolving-systems/telegram-client/main/k8s/deployment.yaml

Five minutes, and you have a new capability. The service connects to Temporal, registers its activities, and every workflow in your system can now send Telegram messages.

You take what you need. If something is missing, you build it — or the AI builds it for you. Contribute it back. The library grows.

This isn’t about one person’s system. It’s about a shared foundation. Universal building blocks that work for any project, any team, any language.

The services themselves are simple. The power comes from how you compose them.

Addressing the skeptics

I’ve discussed this architecture with enough people to know the objections. Let me address them directly.

“This is overengineering”

For a simple CRUD app? Absolutely.

For a system that needs to:

  • Run multiple AI tasks in parallel
  • Scale across teams
  • Extend itself with new capabilities
  • Operate autonomously

This is appropriate engineering. Start with 2–3 services. Add more as you need them. The architecture scales; monoliths don’t.

“Temporal adds complexity”

temporal server start-dev

That’s local development. For production, one Helm chart.

Compare that to building your own:

  • Message queue
  • State management
  • Retry logic
  • Observability
  • Workflow orchestration

Temporal gives you all of that. Out of the box.

“Microservices create their own problems”

They do. Network latency. Distributed tracing. Service discovery.

Temporal solves the hard ones:

  • Observability: Built-in UI shows every workflow step
  • Retries: Automatic, configurable
  • State: Persisted, survives restarts
  • Discovery: Services register themselves

The complexity you’re adding is managed complexity.

“AI can’t write production code”

AI struggles with large, complex codebases. Too many decisions, too much context.

AI does well with small, focused microservices. Clear scope, clear contract, clear boundaries.

That’s the entire point of this architecture — make tasks AI-sized.

“What about MCP servers?”

MCP servers can be part of this ecosystem. Create an MCP-bridge service that proxies MCP tools through Temporal Activities.

But consider: most MCP servers are Node.js. Node.js typically uses 200–400 MB per process. At scale, this affects both performance and cost.

Go microservices typically run at 20–50 MB. That’s roughly 10x more efficient.

MCP servers work. Just be aware of the tradeoffs.

I’ve shared a framework for thinking about self-evolving systems. But this is an evolving space, and I don’t have all the answers.

  • What capabilities would you add?
  • What problems do you see?
  • What have you tried that worked — or didn’t?

I’m genuinely curious how others are approaching this. The architecture described here addresses the problems I’ve thought through, but different contexts have different needs.

The shift in thinking

Let me leave you with this:

👎 Before:

  • “I need a smarter AI”
  • “Let me add more context to the prompt”
  • “Why does AI break on complex tasks?”
  • Fighting against the architecture

🤔 After:

  • “How do I make tasks AI-sized?”
  • “What’s the contract?”
  • “How do I compose simple blocks?”
  • Working with the architecture

Self-evolving systems aren’t science fiction. They’re an architectural choice. The patterns exist. The tools are available.

The question is whether you’ll design for it.

Pavel Buchnev is a software architect with 15+ years of experience in PHP development and DevOps. He leads the design and scaling of SaaS platforms powered by Kubernetes and Temporal. Maintainer of Spiral Framework and contributor to RoadRunner. Currently focused on AI-assisted development — practicing vibe-coding, building MCP servers, and exploring architectures that let AI and humans work in synergy.

What else do you think such a system would need? What have you tried? Connect with me on LinkedIn — let’s figure this out together.

--

--

Pavel Buchnev
Pavel Buchnev

Written by Pavel Buchnev

CTO | VibeCoding Evangelist | 15+ Years in Open Source. Passionate about developer experience https://github.com/butschster