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
| Namespace | What it covers | Token |
|---|---|---|
Gleanex.Client | search, chat, agents, documents, collections, pins, insights | client |
Gleanex.Indexing | pushing documents, people, groups and permissions into the index | indexing |
Gleanex.Platform | agents, skills and the newer search endpoints | client |
Gleanex.Admin | governance policies, reports, findings, datasource administration | client |
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.