Gleanex

Elixir client for Glean, covering all four of Glean's public APIs. The typed layer is generated from the OpenAPI descriptions Glean publishes at gleanwork/open-api, so it tracks the real API rather than a hand-picked subset.

Install

def deps do
[
{:gleanex, "~> 0.1.0"}
]
end

Use it

config = Gleanex.new(domain: "mycompany", token: System.fetch_env!("GLEAN_API_TOKEN"))
{:ok, response} = Gleanex.search(config, "company holidays")
{:ok, answer} = Gleanex.chat(config, "What are the company holidays this year?")

domain is the backend subdomain Glean gave you, usually your email domain without the TLD. Requests go to https://{domain}-be.glean.com.

With GLEAN_INSTANCE and GLEAN_API_TOKEN exported you can leave the config out and each call falls back to the environment.

Those two shortcuts cover the common case. Everything else is a generated operation:

{:ok, response} =
Gleanex.Client.Search.search(
%{query: "holidays", pageSize: 50},
config: config
)
{:ok, agents} = Gleanex.Client.Agents.search_agents(%{}, config: config)
{:ok, _} = Gleanex.Admin.Governance.listpolicies(config: config)

The config travels in the trailing options, alongside per-call overrides like :receive_timeout, :retry and :req_options.

The four APIs

NamespaceWhat it coversToken
Gleanex.Clientsearch, chat, agents, documents, collections, pins, insightsclient
Gleanex.Indexingpushing documents, people, groups and permissions into the indexindexing
Gleanex.Platformagents, skills and the newer search endpointsclient
Gleanex.Admingovernance policies, reports, findings, datasource administrationclient

Client and Indexing tokens are not interchangeable. Build one config per scope, and a mismatched call fails before it leaves your machine:

indexing = Gleanex.new(domain: "mycompany", token: indexing_token, scope: :indexing)
{:ok, _} = Gleanex.Indexing.Documents.indexdocument(%{document: document}, config: indexing)

Results

Every operation returns {:ok, result} or {:error, %Gleanex.Error{}}, never both. Match on reason to tell failures apart:

case Gleanex.search(config, "holidays") do
{:ok, response} ->
response.results
{:error, %Gleanex.Error{reason: :rate_limited, retry_after: seconds}} ->
back_off(seconds)
{:error, %Gleanex.Error{reason: :problem_detail, problem: problem}} ->
Logger.error(problem.detail)
{:error, error} ->
raise error
end

Successful responses are decoded into structs. Field names are Glean's own camelCase, matching their documentation, so request maps and response structs agree with each other:

response.trackingToken
response.hasMoreResults

Retries and timeouts

Transient failures are retried by default, honouring Retry-After on rate limits. Change the policy globally or for one call:

config = Gleanex.new(domain: "mycompany", token: token, retry: %Gleanex.Retry{max_retries: 5})
Gleanex.search(config, "holidays", retry: Gleanex.Retry.disabled())
Gleanex.search(config, "holidays", receive_timeout: 60_000)

Paging

Cursor-paginated endpoints become a Stream:

config
|> Gleanex.Pagination.stream(&Gleanex.Client.Search.search/2, %{query: "holidays"})
|> Stream.flat_map(& &1.results)
|> Enum.take(100)

Streaming

Chat and agent runs can be consumed as they arrive:

{:ok, chunks} = Gleanex.Streaming.chat(config, %{messages: messages})
{:ok, events} = Gleanex.Streaming.agent_run(config, %{agentId: "abc", input: %{}})
for event <- events do
case Gleanex.SSE.json_data(event) do
{:ok, payload} -> handle(payload)
{:error, _} -> :ok
end
end

Bulk indexing

Bulk uploads are paged, and Glean only swaps in the new batch once it has seen the last page. Gleanex.Bulk drives that protocol:

Gleanex.Bulk.upload(
indexing_config,
&Gleanex.Indexing.Documents.bulkindexdocuments/2,
%{datasource: "mydatasource"},
:documents,
documents,
page_size: 500
)

Telemetry

Every request emits a [:gleanex, :request] span with :api, :operation, :method, :url and, on stop, :status.

Working on Gleanex

The typed layer is generated and committed, so users need no Java, no Docker and no generator dependency.

mix glean.specs # download Glean's descriptions into priv/openapi/
mix glean.gen # regenerate lib/gleanex/{client,indexing,platform,admin}/
mix test --cover # the suite, at an enforced 100% threshold

priv/openapi/.api-version records the exact upstream commit the committed code came from. Regeneration is deterministic: with unchanged descriptions it should leave the working tree clean.

Descriptions are taken from source_specs/ upstream, not final_specs/. The latter has code samples merged in, which inflates the Client API description from under 400 KB to about 19 MB without adding anything a generator can use.

Do not hand-edit anything under lib/gleanex/client, lib/gleanex/indexing, lib/gleanex/platform or lib/gleanex/admin. Naming and rendering are steered from config/config.exs and the plugin in dev/gleanex/generator/processor.ex.

Integration tests

The suite runs against stubs, which prove the library does what Gleanex expects of it, not that this is what Glean expects. A wrong path prefix or a field name that no longer matches the description would pass every stubbed test.

A separate read-only smoke test covers that, against a real deployment. It is excluded unless asked for:

GLEAN_INSTANCE=mycompany GLEAN_API_TOKEN=... mix test --include integration

It only reads, and only through the Client API. The Indexing API writes to a real search index, and a bulk upload replaces the previous batch, so it is left to the stubbed tests rather than pointed at a live deployment.

Releasing

Releases are driven by release-please, run through release-mate with a short-lived GitHub App token.

Every Conventional Commit landed on main is collected into a release pull request that stays open and updates itself. Merging it does four things: bumps @version in mix.exs, rewrites CHANGELOG.md, tags the commit vX.Y.Z and cuts the GitHub release. Nothing to run by hand, and no version to remember to bump.

Which commits appear in the changelog follows release-please-config.json: feat, fix, perf and revert are listed, everything else is recorded but hidden. bump-minor-pre-major keeps breaking changes inside 0.x rather than jumping to 1.0.0, and initial-version makes the very first release 0.1.0 rather than release-please's default of 1.0.0.

Cutting the GitHub release triggers .github/workflows/publish.yml, which runs mix hex.publish --yes. It needs a HEX_API_KEY secret.

That makes merging the release pull request the point of no return: a Hex version can never be reused or withdrawn, only deprecated.

Licence

BSD 2-Clause. Gleanex is not affiliated with or endorsed by Glean.