Skip to content

Commands

Commands are app entry points for developer, operator, and application workflows.

They run through the App lifecycle, use injected dependencies, and are exposed through forj and the App binary.

Running Commands

Use the command name directly for the default app:

bash
forj route:list
forj reports:reconcile
forj worker
forj scheduler

Use the app name first to select an additional app:

bash
forj admin route:list
forj admin reports:reconcile
forj admin worker
forj admin scheduler

Inside a GoForj Project, native GoForj commands take precedence. If no native command matches, GoForj delegates to the active App. Use forj run <command> when you want to force default App command execution explicitly, and use ./bin/<app> <command> when running a built binary.

The command runs inside the App, not as an ad hoc shell script around it.

Create a Command

Create a command for the default App:

bash
forj make:command reports:reconcile

Prefix the generator when an additional app owns the command:

bash
forj admin make:command reports:reconcile

Build and run the generated command before replacing its starter behavior:

bash
forj build
go test ./...
forj reports:reconcile

Expected output includes:

text
ReconcileCmd executed!

For an additional app, use forj admin build and forj admin reports:reconcile.

Replace the starter body with the application workflow and add its service to the constructor. Wire will satisfy the new dependency after its provider is in the App service set.

The make:command reference covers output overrides, removal, and the exact registration changes. Keep command code focused on flags, input translation, output, and calling application services.

Command Responsibilities

Commands are a good fit for:

  • explicit operator tasks
  • one-off maintenance actions
  • local development utilities
  • data reconciliation
  • bootstrap tasks
  • running runtime boundaries such as workers or schedulers

Commands should not become unstructured backdoors around application services.

Context and Cancellation

Generated commands can receive the CLI lifecycle context directly. Pass it to the service instead of replacing it with context.Background():

internal/reports/reconcile_cmd.go
go
// Run delegates reconciliation to the service while preserving CLI cancellation.
func (c *ReconcileCmd) Run(ctx context.Context) error {
	return c.service.Reconcile(ctx)
}

Long-running services should check cancellation between units of work and pass the same context into repositories and clients:

internal/reports/service.go
go
// Reconcile processes pending reports until the work completes or the caller cancels.
func (s *Service) Reconcile(ctx context.Context) error {
	reportIDs, err := s.reports.PendingIDs(ctx)
	if err != nil {
		return err
	}

	for _, reportID := range reportIDs {
		if err := ctx.Err(); err != nil {
			return err
		}
		if err := s.reconcileOne(ctx, reportID); err != nil {
			return err
		}
	}
	return nil
}

The CLI context is cancelled when the command lifecycle stops, including interrupt-driven shutdown. Runtime commands such as HTTP, queue workers, and the scheduler receive the same lifecycle-managed cancellation behavior.

Use context.Background() only at a boundary that genuinely has no caller context. A generated command's Run(ctx context.Context) method already has one.

Next Steps