# Graphiti Agent Guide (v2)

Graphiti is a CLI for building and executing Salesforce GraphQL queries by navigating the schema like a filesystem. A **session** holds the current navigation path and query projection.

## Quick Start

```bash
graphiti agent-guide                    # print this guide (for agents)
graphiti connect <org>                  # download schema (one-time per org)
graphiti connect <org> --refresh        # re-download after deploying new metadata
graphiti new <org> --name myQuery       # create a session
graphiti use myQuery                    # set it as active (persists)
graphiti cd uiapi/query/Case/edges/node
graphiti select Id Subject.value:subject Status.value:status
graphiti set uiapi/query/Case first=10
graphiti check                          # validate
graphiti run                            # execute
```

## Refreshing after a deploy

graphiti caches an org's schema on first connect. After you deploy new metadata (fields, picklist values, objects), force a refresh so graphiti stops serving the stale cache — this invalidates all three caches coherently (on-disk introspection JSON, in-memory parsed schema, and the ObjectInfo cache that backs picklists / filterable / sortable / required-on-create):

```bash
graphiti connect <org> --refresh        # CLI: re-download + clear all caches
```

When driving the **MCP server**, use the `sf_gql_connect` tool — the long-lived server holds an in-memory schema that no query tool can refresh on its own:

```jsonc
// sf_gql_connect
{ "org": "<org>", "forceRefresh": true }
// → { org, instanceUrl, refreshed, cached, durationMs, warnings? }
```

Notes:

- Concurrent refreshes (CLI + MCP) **coalesce** into a single introspection.
- If the refresh fails (network/5xx) it is retried once. On terminal failure the **old cache is kept** and `sf_gql_connect` returns `refreshed: false` with a staleness `warnings[]` entry instead of erroring — keep working on the still-valid cached schema and retry the refresh shortly.
- Exception: a **401/403 auth failure** during introspection (expired/unauthorized session) errors with an `Auth:` prefix instead of the soft staleness warning — a dead session makes even the cached schema unusable, so re-authenticate (`sf org login web --alias <org>`) rather than retrying.
- ObjectInfo is keyed by org alias: refreshing alias `A` does not invalidate ObjectInfo cached under a different alias `B` for the same org (its 1-hour TTL heals it).

## Sharing the schema cache from code

Build tooling (codegen, IDE integrations) should not run its own introspection — import graphiti and reuse the shared cache:

```ts
import { downloadSchemaSdl } from "@salesforce/graphiti";

await downloadSchemaSdl({ org: "<org>", outPath: "schema.graphql", maxAgeMs: 10 * 60_000 });
```

This primes the same instance-URL-keyed cache `connect` uses, writes canonical SDL to `outPath`, and (via `maxAgeMs`) re-introspects only when the cached schema is older than the gate — otherwise it serves the existing cache ("wait once"). See the README's "Programmatic API" section for the full option/result reference.

## Session Resolution

All session commands target the **active session** implicitly. Resolution order:

1. `--session <id>` / `-s <id>` flag (highest priority)
2. `GRAPHITI_SESSION` env var
3. `~/.graphiti/active` file (set by `graphiti use`)

```bash
graphiti use myQuery                    # sets active session
graphiti select Id                      # targets myQuery implicitly
graphiti -s other select Id             # targets "other" for this command only
GRAPHITI_SESSION=other graphiti ls      # env var override
```

## JSON / Agent Mode

Every command supports `--json` for structured JSON output. Set `GRAPHITI_AGENT=1` for all commands:

```bash
graphiti show --json                    # single command JSON
GRAPHITI_AGENT=1 graphiti ls            # all output is JSON
```

Exit codes: 0=success, 1=user error, 2=validation failure, 3=execution failure, 4=auth failure.

---

## Session Management

```bash
graphiti new <org> [--mutation] [--aggregate] [--name <n>] [--force]
graphiti use <session-id-or-name>
graphiti sessions                       # list all sessions
graphiti sessions rm <id-or-name>       # delete a session
graphiti sessions prune --older-than 7d # prune old sessions
graphiti clone [name]                   # duplicate current session
graphiti reset                          # clear all state in current session
```

---

## Navigation

The schema is a tree rooted at `/`. Navigate with `cd`, inspect with `ls`.

```bash
pwd                                     # print current path
cd uiapi/query/Account                  # absolute path (query/ prefix auto-added)
cd edges/node                           # relative path
cd @args                                # enter arguments directory
cd @args/where                          # enter a specific argument
cd /variables                           # jump to variables
cd ..                                   # go up one level
cd /                                    # go to root
ls                                      # list fields (20 by default)
ls -l                                   # long listing with metadata (shows [required] on args)
ls -a                                   # show all fields
ls --search Name                        # filter by name (CamelCase prefix match)
ls Name Owner Status                    # peek inside multiple directories
```

### Shell-safe fragment syntax

Union/interface types use `on:TypeName` instead of `[TypeName]` to avoid shell quoting:

```bash
cd Owner/on:User
select Owner.on:User.Name.value:ownerName
```

---

## Building the Projection

Select leaf fields with **absolute or relative paths**. Dot-path syntax with inline aliases:

```bash
select Id                                       # plain leaf
select Name.value:name                          # dot-path with alias
select Id Subject.value:subject Status.value    # multiple fields
select uiapi/query/Case/edges/node/Id           # absolute path (no cd needed)
select ls                                       # list all selected fields
drop <alias-or-path>                            # remove by alias or path
drop ownerName
drop $filter                                    # remove a variable
```

**Agent best practice**: Use absolute paths to avoid interleaving `cd` with `select`:

```bash
graphiti select \
  uiapi/query/Case/edges/node/Id \
  uiapi/query/Case/edges/node/Subject.value:subject \
  uiapi/query/Case/edges/node/Status.value:status
```

### @optional Directive (Field-Level Security)

Salesforce field-level security (FLS) can cause fields to be absent from the response at runtime. The `@optional` directive tells the API to return `null` instead of erroring when FLS blocks a field. **This should be the standard way to request fields** so FLS issues are handled gracefully.

Apply at selection time with `--optional`:

```bash
select --optional Subject.value:subject Status.value:status
```

Or toggle on already-selected fields:

```bash
optional Subject Status                  # add @optional
optional --remove Subject                # remove @optional
```

The `@optional` directive appears in the rendered query:

```graphql
query {
  uiapi {
    query {
      Case {
        edges {
          node {
            Subject @optional {
              value
            }
            Status @optional {
              value
            }
          }
        }
      }
    }
  }
}
```

In `ls` output, fields with `@optional` are marked with `?`:

```
* Id
?*Subject/
?*Status/
```

In `select ls`, `@optional` fields are tagged:

```
  Subject.value → subject @optional
  Status.value → status @optional
```

In `codegen` output, `@optional` fields become optional TypeScript properties with `| undefined`:

```typescript
Subject?: { value: string | null } | null | undefined;
Status?: { value: CaseStatus | null } | null | undefined;
```

---

## Arguments (set / unset)

Use **inline key=value syntax** for efficiency:

```bash
set first=10                                    # set on current field
set uiapi/query/Case first=10 scope=MINE        # with absolute field path
set where='{"Status":{"eq":"New"}}'             # JSON value
set uiapi/query/Case/edges/node/CaseComments first=50 orderBy='[{"CreatedDate":{"order":"DESC"}}]'
unset first                                     # remove an argument
unset where/Name/like                           # remove nested arg
```

Legacy pair syntax still works:

```bash
set @args/first 10
set @args/where/Name/like "Acme%"
```

### Pagination shorthand (`cursor`)

Add `cursor` to any `set` command to enable cursor-based pagination in one step:

```bash
set uiapi/query/Case first=10 cursor
# Equivalent to:
#   set uiapi/query/Case first=10
#   var $after uiapi/query/Case/@args/after
#   select uiapi/query/Case/pageInfo/hasNextPage uiapi/query/Case/pageInfo/endCursor
```

This defines `$after: String`, binds it to the connection's `after` arg, and auto-selects `pageInfo { hasNextPage endCursor }` so pagination loops work out of the box.

### ObjectInfo Guardrails

When setting arguments, the CLI warns immediately about:

- **Invalid picklist values**: `Warning: "Invalid" is not a known picklist value for Status. Valid values: New, On Hold, ...`
- **Non-filterable fields in where**: `Warning: Description is not filterable per ObjectInfo.`
- **Non-sortable fields in orderBy**: `Warning: Description is not sortable per ObjectInfo.`
- **Create-only fields in update mutations**: `Warning: ParentId is create-only (not updateable).`

---

## Variables (var)

Variables make arguments parameterisable:

```bash
var $filter @args/where                         # auto-infer type and bind
var $limit @args/first 10                       # with default value
var $caseId uiapi/query/Case/@args/where/Id/eq   # bind to a deep arg path
set $filter '{"Name":{"like":"Acme%"}}'         # set runtime value
run --var caseId=500SG00001KZliXYAT             # ad-hoc override at execution
```

---

## Aliases (multiple queries in one request)

```bash
cd uiapi/query
alias newCases Case
set newCases/@args/where '{"Status":{"eq":"New"}}'
set newCases/@args/first 10
select newCases/edges/node/Id newCases/edges/node/Subject/value

alias myCases Case
set myCases/@args/scope MINE
set myCases/@args/first 20
select myCases/edges/node/Id myCases/edges/node/Status/value
```

---

## Review & Execute

```bash
show                                    # full session snapshot
show --json                             # JSON output (most important for agents)
check                                   # validate against schema + Salesforce semantics
run                                     # execute against the org
run --dry-run                           # preview without executing
run --var caseId=500xxx                 # ad-hoc variable override
describe Case                           # inspect SObject with ObjectInfo metadata
codegen                                 # generate client types (default: TypeScript)
codegen --language typescript           # explicit language selection
codegen -l ts --out types.ts --name CaseDetail
```

Supported `--language` values: `typescript` (alias `ts`). Additional languages
may be added over time; run `graphiti codegen --help` to see the current list.

### Semantic Validation (check)

Beyond GraphQL schema validation, `check` also warns about:

- Connection fields without `first` set (optional but recommended for predictable pagination)
- Non-filterable fields in `where` clauses
- Non-sortable fields in `orderBy` clauses
- Empty selection sets on connections
- Missing required fields in mutation inputs

---

## Command Chaining

Run multiple commands in a single invocation (session loaded once):

```bash
graphiti chain -s cases "cd uiapi/query/Case; set first=10; select edges/node/Id; check"
graphiti chain -s cases --json "select Id; set first=10; check"  # JSON array output
```

---

## Describe (SObject Inspection)

```bash
graphiti describe Case                  # by name
graphiti describe                       # auto-detect from current navigation
```

Shows: fields with labels, picklist values, reference targets, required/auto/create-only/filterable/sortable tags, child relationships, filter/orderBy examples.

---

## Key Workflows

### 1. Query with absolute paths (agent-optimized, no cd needed)

```bash
graphiti new vscodeOrg --name caseDetail && graphiti use caseDetail
graphiti select \
  uiapi/query/Case/edges/node/Id \
  uiapi/query/Case/edges/node/Subject.value:subject \
  uiapi/query/Case/edges/node/Status.value:status
graphiti set uiapi/query/Case first=1 where='{"Id":{"eq":"$caseId"}}'
graphiti var $caseId uiapi/query/Case/@args/where/Id/eq
graphiti check && graphiti run --var caseId=500SG00001KZliXYAT
```

### 2. Query with navigation (human-friendly)

```bash
graphiti new vscodeOrg --name accounts && graphiti use accounts
graphiti cd uiapi/query/Account/edges/node
graphiti ls -l Name AnnualRevenue Industry Phone Owner
graphiti select Id Name.value:name AnnualRevenue.value:revenue Industry.value:industry
graphiti set uiapi/query/Account first=10
graphiti set uiapi/query/Account where='{"AnnualRevenue":{"gt":10000}}'
graphiti check && graphiti run
```

### 3. Create a record (mutation)

```bash
graphiti new vscodeOrg --mutation --name createOpp && graphiti use createOpp
graphiti cd uiapi/OpportunityCreate/Record
graphiti select Id Name.value:name StageName.value:stage
graphiti cd uiapi/OpportunityCreate/@args
graphiti var $input input
graphiti set $input '{"Opportunity":{"Name":"New Deal","CloseDate":"2026-06-30","StageName":"Prospecting"}}'
graphiti check && graphiti run
```

### 4. Aggregate query (count/group)

```bash
graphiti new vscodeOrg --aggregate --name orderCounts && graphiti use orderCounts
graphiti select \
  uiapi/aggregate/Order/edges/node/aggregate/Status/value:status \
  uiapi/aggregate/Order/edges/node/aggregate/Id/count/value:orderCount
graphiti set uiapi/aggregate/Order 'groupBy={"Status":{"group":true}}'
graphiti check && graphiti run
```

Aggregate queries use `uiapi/aggregate/<SObject>` instead of `uiapi/query/<SObject>`. Fields live under `edges/node/aggregate/<Field>` and expose `value`, `count`, `countDistinct`, and `grouping`. Use `groupBy` to group results.

### 5. Chained commands (fastest)

```bash
graphiti new vscodeOrg --name caseDetail
graphiti chain -s caseDetail 'select uiapi/query/Case/edges/node/Id uiapi/query/Case/edges/node/Subject.value:subject; set uiapi/query/Case first=1 where={"Id":{"eq":"$caseId"}}; var $caseId uiapi/query/Case/@args/where/Id/eq; check; run --var caseId=500SG00001KZliXYAT'
```

---

## Tips

- `show --json` is the single most important command for agents — one call gives everything.
- `ls -l` shows ObjectInfo metadata: labels, picklist values, reference targets, filterable/sortable.
- `describe Case` shows a complete SObject view with field metadata and examples.
- `codegen` generates client types for the session. `--language typescript` (default, alias `ts`) produces TypeScript with picklist unions from ObjectInfo. Pass `--language <lang>` to target additional languages as they are added.
- Use absolute paths with `select` and `set` to avoid interleaving `cd` calls.
- `select ls` lists all currently selected fields — useful for debugging.
- `undo` reverts the last mutation (cd, select, drop, set, var, alias, reset).
- Pass `-q` / `--quiet` to `select` to suppress confirmation messages in scripts.
- Salesforce wraps field values in typed objects (`StringValue`, `PicklistValue`, etc.). Use dot-path: `select Name.value:name`.
- `on:TypeName` is the shell-safe alternative to `[TypeName]` for fragments.

## Common Pitfalls

### Shell variable expansion with `$`

When using `where.Field=$varName`, **always single-quote** the argument to prevent the shell from expanding `$varName` to an empty string:

```bash
# WRONG — shell expands $status to empty string:
graphiti set uiapi/query/Case where.Status=$status

# CORRECT — single quotes prevent expansion:
graphiti set uiapi/query/Case 'where.Status=$status'
```

The CLI will warn if it detects an empty where value (likely from shell expansion).

### Mutation payloads don't include relationships

Mutation create/update payloads only return the flat record fields. You cannot select related objects (e.g., `Account/Name`) from a mutation result. Use a follow-up query with the returned `Id` to fetch related data.

### User scope limitation

The `User` SObject only supports `scope=EVERYTHING`, not `scope=MINE`. To get the current user, use `where.Id=$userId` and pass the user's ID at runtime.

### Parallel agent builds

When multiple agents build queries concurrently, the global active session causes cross-contamination. Each `new` command changes the active session, so another agent's `select`/`set` commands silently target the wrong session.

**Always use `-s` when running in parallel:**

```bash
# WRONG — agents race on the active session:
graphiti new vscodeOrg --name query-a --force
graphiti select uiapi/query/Case/edges/node/Id  # may target query-b!

# CORRECT — explicit session targeting:
graphiti new vscodeOrg --name query-a --force
graphiti -s query-a select uiapi/query/Case/edges/node/Id
graphiti -s query-a set uiapi/query/Case first=10
graphiti -s query-a check --codegen
```

For sequential single-agent workflows, the active session is safe to use implicitly.
