-
Notifications
You must be signed in to change notification settings - Fork 0
Library Inline Mode
Inline mode runs GraphQL operations directly in-process against a gqlgen schema — no HTTP server needed. It's ideal for CLIs that ship alongside your application binary.
- Building a CLI for your own Go application
- You have a gqlgen schema and want a CLI interface without running an HTTP server
- Building tools for AI agents — the schema becomes self-documenting CLI capability
- Embedding a GraphQL-based admin interface in a binary
For connecting to external APIs over HTTP, see Library-HTTP-Mode.
go get github.com/wricardo/gqlcli
go get github.com/99designs/gqlgen # if you don't already have gqlgenpackage main
import (
"log"
"os"
"github.com/urfave/cli/v2"
gqlcli "github.com/wricardo/gqlcli/pkg"
"github.com/myorg/myapp/graph" // your gqlgen package
)
func main() {
// 1. Create your gqlgen resolver and schema.
r := graph.NewResolver()
execSchema := graph.NewExecutableSchema(graph.Config{Resolvers: r})
// 2. Create an inline executor.
exec := gqlcli.NewInlineExecutor(execSchema,
gqlcli.WithSchemaHints(),
)
// 3. Create the command set.
commands := gqlcli.NewInlineCommandSet(exec)
// 4. Mount on any urfave/cli app.
app := &cli.App{
Name: "myapp",
Usage: "CLI for my application",
}
commands.Mount(app)
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}This adds:
myapp query '{ books { id title } }'
myapp mutation 'mutation { addBook(input: {title: "Go", authorName: "Donovan"}) { id } }'
myapp batch < operations.ndjson
myapp describe Book
myapp typesWithSchemaHints() attaches a compact SDL description of the relevant type to GraphQL validation errors:
Error: Cannot query field "titl" on type "Book".
Schema hint:
type Book {
id: ID!
title: String!
author: Author!
}
This helps both humans and AI agents understand what went wrong and how to fix the query.
| Command | Description |
|---|---|
query |
Execute a query in-process |
mutation |
Execute a mutation in-process |
batch |
Execute multiple NDJSON operations in-process |
describe TYPE |
Print SDL for a type |
types |
List all schema types |
login |
(optional) Authenticate and save a session token when configured with WithLogin
|
logout |
(optional) Clear the saved session token |
whoami |
(optional) Show information about the current token |
Returns the SDL definition of a named type, useful for agents to explore the schema before constructing queries. A similar describe command also exists in HTTP mode; in inline mode it runs directly against your in-process schema:
myapp describe Query
# type Query {
# books: [Book!]!
# book(id: ID!): Book
# }
myapp describe AddBookInput
# input AddBookInput {
# title: String!
# authorName: String!
# }Traditional CLIs force users and agents to memorize custom flag syntax:
# Traditional
myapp --type books --filter author=Kernighan --limit 10 --output jsonWith a GraphQL-native CLI:
# GraphQL-native — one universal syntax
myapp query '{ books(filter: {author: "Kernighan"}, limit: 10) { id title author { name } } }'| Traditional CLI | GraphQL-Native CLI |
|---|---|
| Custom flags per command | One universal query syntax |
| AI must learn your CLI | AI already knows GraphQL |
| Hard to combine operations | Multi-query in one call |
| Schema is implicit | Schema is explicit and queryable |
Your schema becomes the CLI interface. Define the API once, use it everywhere:
type Query {
books: [Book!]!
book(id: ID!): Book
}
type Mutation {
addBook(input: AddBookInput!): Book!
}
type Book {
id: ID!
title: String!
author: Author! @goField(forceResolver: true)
}
input AddBookInput {
title: String!
authorName: String!
}Each Query field is a readable operation. Each Mutation field is a writable operation. describe exposes any type's structure on demand.
See Example-Application for a complete working example using this pattern.
InlineCommandSet can be extended with login/logout/whoami support and a persistent token store:
ts := gqlcli.NewTokenStore("myapp")
exec := gqlcli.NewInlineExecutor(execSchema, gqlcli.WithSchemaHints())
commands := gqlcli.NewInlineCommandSet(exec,
gqlcli.WithTokenStore(ts),
gqlcli.WithLogin(gqlcli.LoginConfig{
Mutation: "mutation Login($email:String!,$password:String!){ login(email:$email,password:$password){ token } }",
ExtractToken: func(data map[string]interface{}) (string, error) {
// Walk into data to find your token, e.g. data["login"].(map)["token"].(string)
// (implementation omitted here for brevity)
return "", nil
},
}),
)This wiring adds login, logout, and whoami commands and persists the JWT token at ~/.myapp/token. You can then use WithContextEnricher on the InlineExecutor to inject the token into a context for resolvers.
You can mount inline commands alongside your own custom commands:
app := &cli.App{Name: "myapp"}
// Add your own commands
app.Commands = append(app.Commands, &cli.Command{
Name: "import",
Action: func(c *cli.Context) error {
// custom import logic
return nil
},
})
// Add gqlcli inline commands
commands.Mount(app)
app.Run(os.Args)Schema hints attached to validation errors are capped at 10,000 characters to prevent overwhelming output when a type has many fields.