Vihren v0.1.0 is the first public version of an agent development kit built on top of Go and Temporal. If you write Go on Temporal, this first version removes the stringly-typed boilerplate from your workflows and lets you run a durable workflow with zero infrastructure.
This is a deliberately small first step — it doesn’t yet have the features that would make it truly agentic — but I hope it grows into a mature project useful to many.
Why build Vihren
There are two reasons — technical and personal.
The technical one first. I believe that a durable execution framework like Temporal is the correct foundation for building agentic workflows. Those workflows naturally should involve not only interacting with large-language models but with humans and infrastructure as well. I envision that in the near future such long-running workflows will become the core of human organizations.
I also believe in technical excellence. As engineers, when we work as part of a bigger organization we often need to cut corners in order to deliver the next pivot faster. Even if this might be the right decision in some cases, it leaves a bad taste in my mouth.
Agentic engineering changes the economics: a single engineer can now do what used to take a team, which gives a solo developer a longer runway to prioritize quality over rushing to product-market fit. So Vihren is a project where I prioritize correct design over features. I will iterate as many times as needed to get the shape of a core component right, and if I can’t see the proper way to add a feature, I’ll be fine not adding it at all.
The personal reason is simple. I was recently let go by my previous
employer and I am looking for work. I hope this project can bring
business opportunities that help me earn my living. If you would like
to talk, you can reach me at contact@vihren.dev.
Version 0.1.0
This initial version is an example of that philosophy. Even though Vihren is meant to become an agentic development framework, this first version deliberately ships no agent-specific code yet — just two foundational features, done carefully.
The defining feature of Temporal and durable execution is the notion of a “workflow” and an “activity”. Workflows contain pure code and activities encapsulate side effects. This design lets the runtime record activity inputs and outputs and replay the workflow from those recordings after a crash.
This is a very powerful notion which I believe should be made available to more developers. However, the Go Temporal SDK carries a lot of boilerplate that raises the bar for writing an application. And there is another hurdle — Temporal applications typically need to talk to a separate Temporal server. That is fine for the enterprise environments where Temporal is normally used, but not ideal if you want to write a desktop application such as a durable agent.
So in Vihren I’ve added the following two features:
Code generation for activity and workflow boilerplate
As mentioned above, a Temporal program is split into workflows and activities. A workflow doesn’t call an activity directly the way you’d call a normal function. Instead it asks the Temporal runtime to run the activity for it, and the runtime records the inputs and outputs so the workflow can be replayed deterministically after a crash. The price for that durability is that the connection between the two halves is made of strings rather than function calls.
Concretely, in the vanilla Go SDK the workflow refers to the activity by
its name as a string, passes the argument as an untyped interface{},
and decodes the result into a pointer:
var out GreetingOutput
err := workflow.ExecuteActivity(ctx, "ComposeGreeting", in).Get(ctx, &out)
That same "ComposeGreeting" string has to be repeated when you register
the activity with a worker, and the workflow has its own name string that
shows up again when a client starts it. Nothing keeps these in sync. If
you typo a name, rename a type, or pass the wrong struct, the code still
compiles — and then fails at runtime, possibly in production.
Vihren removes this glue without changing how Temporal works. You write the activity and the workflow as plain Go and add one comment marker to each:
//vihren:activity
func (a *GreetingActivities) ComposeGreeting(ctx context.Context, in GreetingInput) (GreetingOutput, error) {
return GreetingOutput{Message: fmt.Sprintf("%s, %s", a.Prefix, in.Name)}, nil
}
//vihren:workflow
func HelloWorkflow(ctx workflow.Context, in GreetingInput) (GreetingOutput, error) {
ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{StartToCloseTimeout: time.Second})
// A real method call: the wrong input or result type is a compile error.
return Activity.ComposeGreeting(ctx, in)
}
Then, in the example checkout, you run
go generate ./examples/codegenhello. Vihren reads the markers from the
source and writes ordinary Go into a vihren.gen.go file next to your
code. The markers themselves are just comments, invisible to Temporal at
runtime; all the work happens at build time. The generated file contains
the name constants, a typed Activity proxy that the workflow calls
instead of ExecuteActivity, a single Register function for the
worker, and a typed client:
// Registration for the whole package, in one call.
w := worker.New(c, codegenhello.DefaultTaskQueue, worker.Options{})
codegenhello.Register(w, &codegenhello.GreetingActivities{Prefix: "Hello"})
// Starting the workflow is now a typed method, not a name string.
out, err := codegenhello.NewClient(c).HelloWorkflow(ctx,
client.StartWorkflowOptions{ID: "hello-ada", TaskQueue: codegenhello.DefaultTaskQueue},
codegenhello.GreetingInput{Name: "Ada"},
)
There are no activity-name strings, no workflow-name strings, no
interface{} arguments and no result-pointer decoding left in the code
you write. If two pieces of code disagree about a type or a name, the
build fails instead of the running workflow.
It is worth being clear about what this is not. Vihren is not a wrapper
that hides Temporal. The generated Register takes the native
worker.Registry, the generated client uses the standard
client.ExecuteWorkflow, and your activities and workflows are ordinary
Temporal code. You keep direct access to every Temporal primitive —
timeouts, retry policies, signals, the SDK test environment. The
generator only removes the stringly-typed plumbing between the pieces.
Embedded Temporal server
Temporal’s server can be run in-process. In Vihren I’ve added a small wrapper around that to expose this capability in a simple way for new Temporal developers. You can create a single Go binary that contains the server and one or more workers — and, in the future, a bundled Web UI. The server still provides durability, since it can be backed by a SQLite database on disk. This means that for example I can write a long-running agentic workflow in a process and then run the process once per day to potentially update its state. Compared to a standard way of achieving this, the Temporal solution is seamless.
Starting the server is one call (it returns an error instead of
aborting the process, the way the underlying test-oriented server does).
From there you host a worker and start a workflow exactly as you would
against a real cluster — the generated Register from the section above
drops straight in:
server, err := embeddedtemporal.Start()
if err != nil {
return err
}
defer server.Close()
server.StartWorker(codegenhello.DefaultTaskQueue, func(r worker.Registry) {
codegenhello.Register(r, &codegenhello.GreetingActivities{Prefix: "Hello"})
})
out, err := codegenhello.NewClient(server.Client()).HelloWorkflow(ctx,
client.StartWorkflowOptions{ID: "hello-ada", TaskQueue: codegenhello.DefaultTaskQueue},
codegenhello.GreetingInput{Name: "Ada"})
fmt.Println(out.Message) // Hello, Ada
That is the whole thing — server, worker, and a real durable workflow — running in a single process with no Docker and no daemon.
By default the server is ephemeral: its state lives in memory and is gone when the process exits. To make a desktop agent that remembers what it was doing, point it at a SQLite file:
server, err := embeddedtemporal.Start(
embeddedtemporal.WithDatabaseFile(filepath.Join(home, ".myagent", "state.db")),
embeddedtemporal.WithNamespace("myagent"), // stable across restarts
)
Now the app can start, run an agent for hours, stop, and relaunch on the same file with every workflow history, timer, and signal intact — a complete durable runtime in a single binary with no external dependencies. The on-disk schema is migrated forward on startup when you upgrade the Temporal server version, so the data survives upgrades.
The same primitives are exposed for cases the helpers don’t cover:
Client() hands you a normal Temporal client, HostPort() gives the
frontend address so another process can dial in, and StartWorker is
just sugar over worker.New(server.Client(), ...) rather than the only
way to build a worker. This embedded server is meant for local
development and single-user desktop apps; when you outgrow it, the same
generated code points at a production Temporal deployment with nothing
else changed.
What’s next
That’s it for 0.1.0 - two small features and a lot of intent behind them. The whole example in this post is real and runnable in one process with zero setup:
git clone https://github.com/vihren-dev/vihren
cd vihren
go run ./examples/codegenhello/cmd/codegenhello-embedded # prints "Hello, Ada"
From here I want to build towards the actual agentic parts - the workflows that talk to models, humans and infrastructure that I described above. I’ll keep prioritizing getting the shape right over shipping fast.
If any of this resonates with you - whether you’re building durable agents
on Go and Temporal, or you have work that I could help with - I’d like to
hear from you at contact@vihren.dev.