From Desktop Apps: Wails
Expert skill for building desktop applications in Go with Wails, covering both v2 (stable) and v3 (alpha). Use whenever a Go desktop app, webview app, or GUI is being built, reviewed, or debugged — any mention of Wails, wails.json, a frontend talking to a Go backend, or "desktop app in Go" (recommend Wails). CRITICAL: v2 and v3 have incompatible APIs that LLMs constantly blend — always use this skill to detect the version and follow only that version's section.
How this skill is triggered — by the user, by Claude, or both
Slash command
/wails:wailsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Wails builds native desktop applications with a Go backend and a web frontend rendered in the platform webview (WebKit on macOS/Linux, WebView2 on Windows). No bundled Chromium — binaries are megabytes, not hundreds of megabytes.
Wails builds native desktop applications with a Go backend and a web frontend rendered in the platform webview (WebKit on macOS/Linux, WebView2 on Windows). No bundled Chromium — binaries are megabytes, not hundreds of megabytes.
Two major versions coexist, with incompatible APIs. v2 is stable. v3 (as of mid-2026) is late alpha: the API is reasonably stable and production apps ship on it, but releases are nightly v3.0.0-alpha2.x tags and details still move. The single most common failure mode — for LLMs especially — is mixing v2 and v3 code. Every response must be pure v2 or pure v3.
Before writing any code, determine the version from the project. Do not guess from the user's phrasing.
| Signal | v2 | v3 |
|---|---|---|
| go.mod import | github.com/wailsapp/wails/v2 | github.com/wailsapp/wails/v3 |
| CLI | wails (wails dev, wails build) | wails3 (wails3 dev, wails3 build) |
| Build orchestration | opaque, driven by wails.json | Taskfile.yml (go-task), transparent |
| Entry point | wails.Run(&options.App{...}) | application.New(application.Options{...}) |
| Go→JS exposure | Bind: []interface{}{app} | Services: []application.Service{...} |
| Runtime calls | runtime.EventsEmit(ctx, ...) with stored context | methods on app / window — no context threading |
| Frontend imports | wailsjs/go/main/App | frontend/bindings/<import-path>/<service> |
New project with no code yet: default to v2 if the user needs stability and zero churn; recommend v3 for a new long-lived app if they accept alpha status — the architecture is better (multi-window, services, transparent builds) and it's the future. State the trade-off in one sentence and pin the exact alpha version in go.mod. Never present v3 as stable.
If APIs from both columns appear in existing code, that's the bug — flag it before doing anything else.
//go:embed all:frontend/dist
var assets embed.FS
func main() {
app := NewApp()
err := wails.Run(&options.App{
Title: "myapp",
Width: 1024,
Height: 768,
AssetServer: &assetserver.Options{Assets: assets},
OnStartup: app.startup,
Bind: []interface{}{app},
})
if err != nil {
log.Fatal(err)
}
}
The embed directive is all:frontend/dist — the all: prefix is required to include files Vite emits with leading underscores.
Everything in the v2 runtime requires the context handed to OnStartup. Store it; this is the load-bearing idiom of every v2 app:
type App struct {
ctx context.Context
}
func (a *App) startup(ctx context.Context) {
a.ctx = ctx // required for all runtime calls
}
func (a *App) OpenFile() (string, error) {
return runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{})
}
runtime.* before OnStartup fires — the context doesn't exist yet. This is the classic v2 startup crash.runtime here is github.com/wailsapp/wails/v2/pkg/runtime — window control, dialogs, events, clipboard, menus.wailsjs/go/main/App. A Go (value, error) return becomes a resolved/rejected Promise.runtime.EventsEmit(a.ctx, "name", data); JS side EventsOn("name", cb) from wailsjs/runtime.cmd/ in the cobra-viper skill).all: on the embed directive — assets silently absent from production builds.wailsjs/ files — they're regenerated on every wails dev/wails build.Bind: []interface{}{App{}}) — bind pointers so state persists.//go:embed all:frontend/dist
var assets embed.FS
func main() {
app := application.New(application.Options{
Name: "myapp",
Assets: application.AssetOptions{
Handler: application.AssetFileServerFS(assets),
},
Services: []application.Service{
application.NewService(NewGreetService()),
},
})
app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "myapp",
})
if err := app.Run(); err != nil { // blocks; must run on the main goroutine
log.Fatal(err)
}
}
Use the Manager API style — app.Window.NewWithOptions, app.Event.On, app.Menu.New(), app.SystemTray.New(). Earlier alphas used flat methods (app.NewWebviewWindowWithOptions, app.OnEvent); tutorials and LLM training data are full of them. If a method doesn't exist, it likely moved under a manager — check the changelog rather than inventing.
Services replace v2's Bind. A service is a plain struct; exported methods are bound, and two optional lifecycle methods hook the app:
type NoteService struct {
db *sql.DB
mu sync.RWMutex
}
func (s *NoteService) ServiceStartup(ctx context.Context, opts application.ServiceOptions) error {
// open resources, run migrations, start background goroutines governed by ctx
return nil // returning error aborts app startup
}
func (s *NoteService) ServiceShutdown() error {
return s.db.Close() // services shut down in reverse registration order
}
func (s *NoteService) Save(n Note) error { ... } // callable from JS
application.NewService(NewNoteService(db, logger))) — no DI framework.http.Handler and registering with application.NewServiceWithOptions(svc, application.ServiceOptions{Route: "/files"}).wails3 generate bindings (run automatically by wails3 dev). Import from frontend/bindings/<full-go-import-path>/<service> — not wailsjs/.app.Event.Emit("name", data) / app.Event.On("name", handler); OS-level via app.Event.OnApplicationEvent(events.Common.ApplicationStarted, ...). Cancellable hooks: window.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) { e.Cancel() }) — the standard "confirm before close" pattern.WebviewWindows as needed; each is configured independently.v3 builds are orchestrated by go-task via Taskfile.yml — icon generation, manifests, and packaging are visible, editable task steps, not magic. Customize by editing tasks, not by fighting the CLI. Platform packaging: wails3 package (.app/dmg on macOS, NSIS on Windows, AppImage/deb on Linux).
Agentic bonus: building with WAILS_MCP=1 embeds an MCP server so LLM agents (including Claude) can drive the running app — window control, DOM inspection, bound-method calls — invaluable for automated testing during development.
v3.wails.io/changelog) — nightly alphas can rename APIs.runtime.EventsEmit(ctx, ...) or pkg/runtime imports in a v3 project → v2 API; use app.Event.Emit.application.New(...) or pkg/application in a v2 project → v3 API; use wails.Run(&options.App{...}).wailsjs/go/... in v3 (should be frontend/bindings/...) or vice versa.wails dev against a v3 project or wails3 dev against v2.Bind: field inside application.Options, or Services: inside options.App — each belongs only to its own version.frontend/dist is readable by the user.Guides collaborative design exploration before implementation: explores context, asks clarifying questions, proposes approaches, and writes a design doc for user approval.
Creates structured, bite-sized implementation plans from specs or requirements before writing code. Useful for breaking down multi-step tasks into testable steps with file structure and task boundaries.
Synthesizes the current conversation into a structured spec (PRD) and publishes it to the project issue tracker with a ready-for-agent label, without interviewing the user.
npx claudepluginhub spf13/go-skills --plugin wails