# Discovery: How an Agent Finds Your APIs

> Source: <https://dev.to/newadventuresinit/discovery-how-an-agent-finds-your-apis-hh7>
> Published: 2026-07-07 03:32:50+00:00

[Last time](https://dev.to/newadventuresinit/neander-and-grotto-beyond-code-mode-21im) I made the case for why **Neander** and **Grotto** exist at all — a purpose-built, safe-by-construction language instead of a sandboxed general-purpose one. That was the argument for the whole language. From here, **Field Notes from the Grotto** takes it apart one feature at a time, and the first one is the feature that explains why the rest exists: discovery.

Classical integration had a shape we all recognize: a developer reads one system's API documentation, then writes integration code that calls it in the right order with the right data. Two steps, one human. Make the calling system agentic — let it decide at runtime what it needs — and the human in the middle vanishes. The two steps do not. They have to land somewhere.

The writing step is the half everyone talks about: the agent writes the program instead of the developer. It is the *reading* step that gets skipped over. Before you can write a line against an API you have to find out what the API even is — what exists, what it takes, what it returns. That was the developer with a browser tab open on the documentation. When the developer leaves, that finding-out does not leave with them; something has to inherit it. That something is discovery.

The eager answer is the tool catalog: hand the agent every function definition up front and let it pick. That is discovery too — just total, and paid in advance. It does not scale. Hundreds of definitions clutter the context before the agent has done anything, every intermediate result piles on top, and latency and cost climb with them.

Take the pragmatic route from my post [ Source Code as the Seam Between Systems](https://dev.to/newadventuresinit/source-code-as-the-seam-between-systems-5b1p) — a general-purpose language the model already writes fluently, its APIs behind a sandbox — and you inherit a quieter awkwardness. That language and those API packages were built for human developers, and human developers read documentation: a reference site, a PDF, a README, a page of generated typedocs. But a running agent is not a human developer. It may not have unrestricted access to the web to search for and reach that reference page at all — and even where it does, the page is shaped for a person: prose, worked examples, a layout to skim, none of it meant to be consumed by an agent. So every system that takes this route has to bolt something on — a meta-tool that lists the available functions, or a search endpoint the agent queries before it writes anything. It works. But it is an appendage: out of band, and reinvented for every stack.

Neander makes discovery part of the language itself. `discover`

is a verb you write into a program and submit exactly the same way you submit a program that does real work — and that is the unification worth stressing. In Neander, *everything is a program*. Finding out what exists and calling it are the same kind of act, in the same language, through the same entry point. The runtime exposes one operation to the agent — `submitProgram`

— and every interaction, whether the agent is asking what is available or getting something done, flows through it. With this, the finding-out moved in-band. What the developer used to do with a browser tab, the agent now does with a program.

Almost everything in Neander exists to glue two verbs together. `call`

invokes one of the registered API functions. `discover`

asks the runtime what there is to call.

`discover`

has six forms — three things you can look for (namespaces, functions, documents) crossed with two ways to look (search a list, or get one by exact name):

```
discover namespaces ["payment"]              // [Namespace]  — search
discover namespace  "shipping"               // Namespace?   — exact lookup of a namespace
discover functions  ns ["estimate", "intl"]  // [Function]   — search the namespace's functions
discover function   ns "estimateBatch"       // Function?    — exact lookup of a function
discover documents  ns ["format"]            // [Document]   — search the namespace's documents
discover document   ns "requestFormat"       // Document?    — exact lookup of a document
```

Search terms are case-insensitive substrings, AND-combined, matched against each candidate's name and description. The empty list matches everything. That is the entire discovery surface.

A cold agent does not know your API, so it works inward. The power lies in the sequential writing and execution of code. One program to list namespaces, a second to look for functions inside a namespace, a third to make the function call — reading and learning from the response envelopes the runtime returns as the result of every program submission.

``` php
neander 1 {
  types {}
  main -> [Namespace] {
    return discover namespaces []
  }
}
```

The runtime answers with a standard envelope — a `success`

flag, the `result`

, and a `meta`

block of telemetry. The return value of type `[Namespace]`

is the fully serialized list of available namespaces:

```
{
  "success": true,
  "result": [
    {
      "name": "bookings",
      "description": "Booking management — retrieve, list, confirm, and escalate reservations for the hospitality backend"
    },
    {
      "name": "guests",
      "description": "Guest profile lookup — retrieve and list guests for the hospitality backend"
    },
    {
      "name": "payments",
      "description": "Payment processing — charge bookings, issue refunds, and look up prior charges"
    }
  ],
  "meta": {
    "thalersConsumed": 1,
    "memoryConsumedKb": 1,
    "durationMs": 2,
    "apiCalls": []
  }
}
```

With this list in its context the agent can now further inspect the namespace of interest:

``` php
neander 1 {
  types {}
  main -> [Function] {
    let ns: Namespace =? discover namespace "bookings"
    return discover functions ns []
  }
}
```

This time the result contains the list of available functions in this namespace, each function serialized in full (only one function shown here):

```
{
  "success": true,
  "result": [
    {
      "qualifiedName": "bookings.get",
      "description": "Get a booking by its numeric id",
      "params": {
        "id": "int"
      },
      "returnType": "bookings.Booking",
      "errors": {
        "404": "Booking not found"
      },
      "types": {
        "bookings.Booking": {
          "description": "A reservation record with id, fare amount, state, and the guest who placed it",
          "fields": {
            "id": "int",
            "fare": "decimal(2, half_away)",
            "state": "string",
            "guestId": "int"
          }
        }
      }
    }
  ],
  "meta": {
    "thalersConsumed": 2,
    "memoryConsumedKb": 1,
    "durationMs": 3,
    "apiCalls": []
  }
}
```

Every function name listed is the exact string to write next — `qualifiedName`

is what you pass to `call`

, and the `types`

side-table is transitively complete, so the shape of `bookings.Booking`

arrives with the function that returns it. The agent reads it straight out of the JSON response envelope, then writes the program that does the work:

``` php
neander 1 {
  types {}
  main -> decimal(2, half_away) {
    let booking: bookings.Booking =? call bookings.get(id: 8821)
    return booking.fare
  }
}
```

Discover, call, return. Everything else in the language is detail.

The design choice worth pointing out is that the values `discover`

returns — handles of type `Namespace`

, `Function`

, `Document`

— are *opaque*. A program can hold one and return it, but it cannot read its fields. The envelope contains what the runtime serializes when the discovery handle is *returned* from `main`

. Inside the program the handle is a sealed token. Only on the way out does it become readable.

What might sound like a restriction is, in fact, a guarantee. A discovery handle can only ever come *from* `discover`

— you cannot forge one out of a record literal or return it from an API function. So a discovery handle the agent sees is always a handle the runtime minted, pointing at something the runtime actually registered. Discovery is not a convention, it is the only way in.

Discovery answers *what APIs exist* — but one question remains open: how does an agent discover discovery? The disadvantage of a new, purpose-built language like Neander is that a cold-start agent has never heard of it in stark contrast to any well-established general-purpose language. So before the agent can write even its first three-line namespace discovery program, it has to learn the language itself with nothing more at its disposal than the `submitProgram`

operation. If you do not know the language, what is the only program you can write which is not guesswork?

The empty program, exactly!

In the meantime, read the [Neander](https://newadventuresinit.github.io/neander/) spec, embed [Grotto](https://github.com/newadventuresinit/grotto) in your own app, and let me know what you discovered.
