# Give a tool per-project values

> Declare the env values a tool needs, set them for a tenant, an org or a project, and read them from the call's context.

This guide gives a tool values that differ per project, such as a base URL or a region, and reads them in the tool's code.

## Before you start

- **A pack,** on a runtime such as `kindgi dev`, with Kindgi 0.1's CLI. This guide uses the quickstart's pack, `my-pack`.
- **A project's id,** to set a value for one project. `kindgi projects list` lists the projects with their ids.

## Declare the values

Some of what a tool needs differs from one customer or project to the next, without being secret: a base URL, an account or region id, a queue name. A tool declares these **env values** by name. You set them for the whole tenant, an org or a single project. On every call, Kindgi hands the tool the most specific value in the call's context.

Ask your coding agent: "I want a tool that looks up an order in our order system. Its address and region differ per project." Kindgi's skills in your pack tell it how ([How it knows Kindgi](https://docs.kindgi.com/v0.1.9/start/coding-agents.md)). For reference, the tool looks like this:

- **TypeScript**

  `tools/find-order/index.ts`

  ```ts
  import { defineTool } from '@kindgi/sdk/define';
  import type { ToolId } from '@kindgi/sdk/types';
  import { z } from 'zod';

  const defined = defineTool({
    id: 'my-pack.find-order' as ToolId,
    description: 'Finds an order in the order system.',
    version: '0.1.0',
    input: z.object({ orderId: z.string() }),
    output: z.object({ url: z.string() }),
    effects: [],
    mutating: false,
    needsSpec: {
      env: {
        ORDERS_BASE_URL: { type: 'string', pattern: '^https://' },
        ORDERS_REGION: { type: 'string', enum: ['eu', 'us'], default: 'eu' },
      },
    },
    handler: async ({ orderId }, ctx) => {
      const base = ctx.env?.ORDERS_BASE_URL;
      if (base === undefined) throw new Error('ORDERS_BASE_URL is not set');
      return { url: `${base}/${ctx.env?.ORDERS_REGION}/orders/${orderId}` };
    },
  });

  if (defined.kind === 'err') {
    throw new Error(`my-pack.find-order failed to compile: ${defined.error.message}`);
  }

  export default defined.value;
  ```

- **Python**

  `tools/orders.py`

  ```python
  from pydantic import BaseModel, Field

  from kindgi import ToolContext, tool


  class Lookup(BaseModel):
      order_id: str = Field(alias="orderId")


  class Found(BaseModel):
      url: str


  @tool(
      id="my-pack.find-order",
      mutating=False,
      needs_spec={
          "env": {
              "ORDERS_BASE_URL": {"type": "string", "pattern": "^https://"},
              "ORDERS_REGION": {"type": "string", "enum": ["eu", "us"], "default": "eu"},
          }
      },
  )
  def find_order(lookup: Lookup, ctx: ToolContext) -> Found:
      """Finds an order in the order system."""
      base = ctx.env["ORDERS_BASE_URL"]
      return Found(url=f"{base}/{ctx.env['ORDERS_REGION']}/orders/{lookup.order_id}")
  ```

- **Java**

  `src/main/java/mypack/tools/FindOrder.java`

  ```java
  package mypack.tools;

  import com.kindgi.pack.Tool;
  import java.util.List;
  import java.util.Map;

  public final class FindOrder {
    public record Input(String orderId) {}

    public record Output(String url) {}

    public static final Tool<Input, Output> TOOL = Tool.define("my-pack.find-order")
        .description("Finds an order in the order system.")
        .version("0.1.0")
        .input(Input.class)
        .output(Output.class)
        .mutating(false)
        .set("needsSpec", Map.of("env", Map.of(
            "ORDERS_BASE_URL", Map.of("type", "string", "pattern", "^https://"),
            "ORDERS_REGION", Map.of("type", "string", "enum", List.of("eu", "us"), "default", "eu"))))
        .handler((input, ctx) -> {
          Object base = ctx.env().get("ORDERS_BASE_URL");
          if (base == null) throw new IllegalStateException("ORDERS_BASE_URL is not set");
          return new Output(base + "/" + ctx.env().get("ORDERS_REGION") + "/orders/" + input.orderId());
        });

    private FindOrder() {}
  }
  ```

  `needsSpec` goes in with `set("needsSpec", …)`, as the API names it. `ctx.env()` holds the declared values, by name, with a `default` filled in where no scope sets one.

- **Scala**

  `src/main/scala/mypack/tools/FindOrder.scala`

  ```scala
  package mypack.tools

  import com.kindgi.pack.scaladsl._

  object FindOrder {
    final case class Input(orderId: String)
    final case class Output(url: String)

    val tool: Tool[Input, Output] = Tool[Input, Output]("my-pack.find-order")
      .description("Finds an order in the order system.")
      .version("0.1.0")
      .readOnly
      .set("needsSpec", Map("env" -> Map(
        "ORDERS_BASE_URL" -> Map("type" -> "string", "pattern" -> "^https://"),
        "ORDERS_REGION" -> Map("type" -> "string", "enum" -> List("eu", "us"), "default" -> "eu"))))
      .handler { (in, ctx) =>
        val base = Option(ctx.env.get("ORDERS_BASE_URL"))
          .getOrElse(throw new IllegalStateException("ORDERS_BASE_URL is not set"))
        Output(s"$base/${ctx.env.get("ORDERS_REGION")}/orders/${in.orderId}")
      }
  }
  ```

  `needsSpec` goes in with `set("needsSpec", …)`, as the API names it. `ctx.env` holds the declared values, by name, with a `default` filled in where no scope sets one.

* **The declaration** maps each name to a JSON Schema for its value. Values are strings, as in a process environment. The schema checks the string, with keywords such as `enum`, `pattern` or `minLength`. A name with a `default` is optional. Every other declared name is required. Each schema must compile as the runtime compiles it, strictly: an unknown keyword is an error. A `default` must be a string. A tool with a schema that doesn't compile is refused when it's defined, registered or deployed:

  ```text
  Tool "my-pack.find-order": the schema for needsSpec.env.ORDERS_REGION doesn't compile: strict mode: unknown keyword: "enmu"
  ```

* **On every call**, Kindgi resolves each declared name for the call: the project's value, else its org's, else the tenant's. It reads them in the env the runtime serves, which `KINDGI_ENV` names. Under `kindgi dev`, the env is `local`. A name no scope sets gets its `default`. A required name with no value, or a value that doesn't fit its schema, fails the call before the handler runs. The failure names the tool and the value, and gives the command that sets it.

* **The values a call used are recorded with it.** A call re-run after a wait or a retry sees the same ones, even after someone changes them. An approval is one such wait. A comparison that replays the run sends a read-only tool it runs live the values the run's call saw, so a changed value can't skew it.

* **The context** holds only what the tool declares. `ctx.env` is optional in the TypeScript type because a unit test builds its own context.

## Set the values

`kindgi env set` with `--scope` stores a value in the runtime, for a scope and an env. Set a value for the whole tenant, and override one for a single project:

- **TypeScript**

  ```sh
  pnpm exec kindgi env set ORDERS_BASE_URL https://orders.example.com --scope=tenant --env=local
  pnpm exec kindgi env set ORDERS_REGION us --scope=project:<project-id> --env=local
  ```

- **Python**

  ```sh
  uv run kindgi env set ORDERS_BASE_URL https://orders.example.com --scope=tenant --env=local
  uv run kindgi env set ORDERS_REGION us --scope=project:<project-id> --env=local
  ```

- **Java**

  ```sh
  ./kindgiw env set ORDERS_BASE_URL https://orders.example.com --scope=tenant --env=local
  ./kindgiw env set ORDERS_REGION us --scope=project:<project-id> --env=local
  ```

- **Scala**

  ```sh
  ./kindgiw env set ORDERS_BASE_URL https://orders.example.com --scope=tenant --env=local
  ./kindgiw env set ORDERS_REGION us --scope=project:<project-id> --env=local
  ```

Each command prints JSON with the value's env, scope, key and revision, and whether it replaced a value, then a line that confirms it. The first ends with:

```text excerpt
…
  ✓ Set ORDERS_BASE_URL at tenant in env local. A tool that declares it (`needsSpec.env`) gets it on its next call.
```

The second prints the same line for `ORDERS_REGION` at `project:<project-id>`.

- **`--scope`** is where the value lives: `tenant`, `org:<orgId>` or `project:<projectId>`.
- **`<project-id>`** is the project's id. A call in that project now gets `ORDERS_REGION=us`. Every other call gets the default, `eu`.
- **`--env`** is required with `--scope`. Name the env the runtime serves, which `KINDGI_ENV` names. Under `kindgi dev`, it's `local`. A value under another env name never reaches a call.
- **A new value counts on the next call.** Nothing restarts, and no new agent version is needed.
- **Changing a value** that's already set takes `--force`.
- **`kindgi env list`**, with a `--scope` and an `--env`, shows that scope's values.
- **`kindgi env unset NAME`**, with the same two flags, removes one. A call then gets the next scope's value again.

### A value nobody set

A call that needs a declared value no scope sets, and that has no `default`, fails before the tool runs. The message names the value and the command that sets it. For a tool `acme-orders.needs-account` that declares `ACME_ACCOUNT_ID`, which nobody set, it reads:

```text
precondition-failed: Tool "acme-orders.needs-account" was not run: env-value-missing: tool "acme-orders.needs-account" needs env value "ACME_ACCOUNT_ID" in env "local", and none is set for project e889c1f5-eae7-45dc-8669-5bd029a5d85c, its org, or the tenant. Set it: kindgi env set ACME_ACCOUNT_ID <value> --scope=project:e889c1f5-eae7-45dc-8669-5bd029a5d85c --env=local (or --scope=tenant, for every project)
```

> **Note**
>
> Without `--scope`, `kindgi env set`, `list` and `unset` edit the pack's own env files. Those files are the pack service's process environment, not `ctx.env`. Under `kindgi dev` too, a tool's env values come only from what `--scope` sets.

## Test it

A test passes the values in the context it builds. Ask your coding agent: "Add a test for the order lookup tool." For reference, the test looks like this.

- **TypeScript**

  `invokeToolForTest` decides them as a run does:

  - a name the test doesn't set takes its schema's `default`;
  - each value is checked against its schema;
  - the handler sees the declared names only.

  A missing or invalid value is `precondition-failed`, with the reason `env-value-missing` or `env-value-invalid`, and the handler doesn't run. Its third argument is the context. What it sets goes over the defaults of a test's context, which `toolContextForTest` builds:

  `tools/find-order/index.test.ts`

  ```ts
  import { invokeToolForTest } from '@kindgi/sdk/define';
  import { expect, test } from 'vitest';

  import findOrder from './index.js';

  test('builds the URL from the env values', async () => {
    const result = await invokeToolForTest(
      findOrder,
      { orderId: 'ord_1001' },
      { env: { ORDERS_BASE_URL: 'https://orders.example.com' } },
    );
    // ORDERS_REGION isn't set, so it's its default, `eu`, as in a run.
    expect(result).toEqual({
      kind: 'ok',
      value: { url: 'https://orders.example.com/eu/orders/ord_1001' },
    });
  });
  ```

- **Python**

  `tests/test_orders.py`

  ```python
  from kindgi import ToolContext
  from tools.orders import Lookup, find_order


  def test_builds_the_url_from_the_env_values():
      ctx = ToolContext.for_test(
          env={"ORDERS_BASE_URL": "https://orders.example.com", "ORDERS_REGION": "us"}
      )
      found = find_order(Lookup(orderId="ord_1001"), ctx)
      assert found.url == "https://orders.example.com/us/orders/ord_1001"
  ```

- **Java**

  `new ToolContext(…)` takes the env values as the handler gets them. Nothing fills in a default or checks a value against its schema, as `invokeToolForTest` does in TypeScript, so the test sets each name the handler reads.

  `src/test/java/mypack/tools/FindOrderTest.java`

  ```java
  package mypack.tools;

  import static org.junit.jupiter.api.Assertions.assertEquals;

  import com.kindgi.log.Logger;
  import com.kindgi.pack.Cancellation;
  import com.kindgi.pack.ToolContext;
  import java.util.Map;
  import org.junit.jupiter.api.Test;

  class FindOrderTest {
    @Test
    void buildsTheUrlFromTheEnvValues() throws Exception {
      Map<String, Object> env = Map.of("ORDERS_BASE_URL", "https://orders.example.com", "ORDERS_REGION", "us");
      ToolContext ctx = new ToolContext("test-tenant", "test-run", null, null, null, env,
          Map.of(), Map.of(), Map.of(), new Cancellation(), Logger.noop());
      FindOrder.Output output = FindOrder.TOOL.call(new FindOrder.Input("ord_1001"), ctx);
      assertEquals("https://orders.example.com/us/orders/ord_1001", output.url());
    }
  }
  ```

- **Scala**

  `new ToolContext(…)` takes the env values as the handler gets them. Nothing fills in a default or checks a value against its schema, as `invokeToolForTest` does in TypeScript, so the test sets each name the handler reads.

  `src/test/scala/mypack/tools/FindOrderSuite.scala`

  ```scala
  package mypack.tools

  import com.kindgi.log.Logger
  import com.kindgi.pack.{Cancellation, ToolContext}
  import java.util.Map as JMap

  class FindOrderSuite extends munit.FunSuite {
    test("builds the URL from the env values") {
      val env = JMap.of[String, Object]("ORDERS_BASE_URL", "https://orders.example.com", "ORDERS_REGION", "us")
      val ctx = new ToolContext("test-tenant", "test-run", null, null, null, env,
        JMap.of(), JMap.of(), JMap.of(), new Cancellation(), Logger.noop())
      val output = FindOrder.tool.call(FindOrder.Input("ord_1001"), ctx)
      assertEquals(output.url, "https://orders.example.com/us/orders/ord_1001")
    }
  }
  ```

## Env values, settings, secrets, process env

A pack's code gets four kinds of values. Pick by what the value is:

| The value…                                                                                                | Use                                                                                           | The code reads               |
| --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | ---------------------------- |
| should change only with a new agent version, judged before it goes live (a threshold, a prompt parameter) | a [settings block](https://docs.kindgi.com/v0.1.9/guides/evals/change-a-prompt-or-setting.md) | `ctx.settings`               |
| differs per tenant, org or project, and isn't secret (a base URL, a region)                               | an env value                                                                                  | `ctx.env`                    |
| is a credential (an API key, a signing key)                                                               | a secret                                                                                      | `ctx.secrets`                |
| is the pack service's own setup (its database, its SDK's config)                                          | the pack's environment                                                                        | `process.env` / `os.environ` |

Env values aren't secret: they're recorded with each run that uses them and shown in its journal. `kindgi env set` warns about a name that looks like a credential, such as `…_TOKEN`, `…_KEY` or `…_PASSWORD`. Store those with `kindgi secrets`, as [Give a tool a secret](https://docs.kindgi.com/v0.1.9/guides/tools/give-a-tool-a-secret.md) shows.

## Next

- [Give a tool a secret](https://docs.kindgi.com/v0.1.9/guides/tools/give-a-tool-a-secret.md)
- [Change a prompt or a setting without a deploy](https://docs.kindgi.com/v0.1.9/guides/evals/change-a-prompt-or-setting.md)
- [Organize work by org and project](https://docs.kindgi.com/v0.1.9/guides/projects/organize-by-org-and-project.md)
