Skip to content

Library Inline Mode

Wallace Ricardo edited this page Apr 11, 2026 · 2 revisions

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.

When to Use This

  • 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.

Setup

go get github.com/wricardo/gqlcli
go get github.com/99designs/gqlgen  # if you don't already have gqlgen

Basic Example

package 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 types

Schema Hints

WithSchemaHints() 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.

Commands Added by InlineCommandSet

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

describe Command

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!
# }

Why GraphQL for CLIs?

Traditional CLIs force users and agents to memorize custom flag syntax:

# Traditional
myapp --type books --filter author=Kernighan --limit 10 --output json

With 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

Integration with gqlgen

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.

Authentication and Token Store (Optional)

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.

Extending Inline Commands

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)

SchemaHints Cap

Schema hints attached to validation errors are capped at 10,000 characters to prevent overwhelming output when a type has many fields.

Clone this wiki locally