> ## Documentation Index
> Fetch the complete documentation index at: https://docs.evox.wraithesports.app/llms.txt
> Use this file to discover all available pages before exploring further.

# First real plugin

> Write a T1 plugin that shows the weather on a key using settings, persistent storage and a host-brokered network call.

This tutorial turns the <a href="/en/sdk/quickstart">Quickstart</a> template into a real plugin: a **Hava** (weather) action that reads a city from a user setting, fetches the weather over host-brokered HTTPS and writes it on the key.

What you will learn: manifest permission declaration (the `network.fetch` host allowlist), binding settings (`settingsSchema` + `action.settingsChanged`), `client.networkFetch` and fail-closed error display.

<Note>
  This example uses the host-brokered `client.networkFetch`; that is for portability to the **T2 (public sandbox)** target. **On the T1 (local dev) host, `network.fetch` is not wired yet** — T1 is sandboxless Node, so do the network call directly: use `const text = await (await fetch(url)).text();` instead of `client.networkFetch({ url, method })` and remove the `network.fetch` permission from the manifest. Which capability works where: <a href="/en/sdk/capabilities">Capability APIs</a>.
</Note>

## 1. Manifest: declare the permission and action

Update the `permissions` and `actions` sections in `manifest.json`:

```json theme={}
{
  "permissions": [
    { "name": "key.display" },
    { "name": "network.fetch", "hosts": ["wttr.in"], "methods": ["GET"] }
  ],
  "actions": [
    {
      "id": "weather",
      "name": "Hava",
      "description": "Ayarlı şehrin hava durumunu gösterir.",
      "controllers": ["Keypad"],
      "supportedInMultiActions": false,
      "settingsSchema": [
        { "key": "city", "type": "text", "label": "Şehir", "required": true }
      ]
    }
  ]
}
```

* `network.fetch` is limited to **the HTTPS hosts declared in the manifest**; a request outside `wttr.in` gets `CAPABILITY_DENIED`.
* The `settingsSchema` field appears in the app's settings panel when the user assigns the action to a key.

## 2. Runtime: event → setting → network → screen

`runtime.mjs`:

```js theme={}
import { EvoxPluginClient } from '@evoxapp/plugin-sdk';
import { createNodeProcessIpcPluginTransport } from '@evoxapp/plugin-sdk/node';

const client = new EvoxPluginClient(createNodeProcessIpcPluginTransport(), {
  sdkVersion: '0.1.0',
  onError: (error) => console.error('[sdk] ' + String(error?.message || error?.code || 'Bilinmeyen hata')),
});

async function renderWeather(context) {
  const bindingId = context.bindingInstanceId;
  try {
    const settings = await client.getSettings(bindingId);
    const city = String(settings?.city || '').trim();
    if (!city) {
      await client.setTitle(bindingId, 'Şehir seçin');
      return;
    }
    // Host-brokered HTTPS: yalnız manifest'teki host + method izinlidir.
    const result = await client.networkFetch({
      url: `https://wttr.in/${encodeURIComponent(city)}?format=%t`,
      method: 'GET',
    });
    const text = String(result?.body ?? result?.text ?? '').trim();
    await client.setTitle(bindingId, text ? `${city}\n${text}` : city);
  } catch (error) {
    console.error('[weather] ' + String(error?.message || error));
    await client.setTitle(bindingId, 'Hata');
    await client.showError(bindingId);
  }
}

client.on('action.willAppear', async (event) => {
  if (event.context?.actionId === 'weather') await renderWeather(event.context);
});

client.on('action.keyDown', async (event) => {
  if (event.context?.actionId === 'weather') await renderWeather(event.context);
});

client.on('action.settingsChanged', async (event) => {
  if (event.context?.actionId === 'weather') await renderWeather(event.context);
});

await client.start();
```

Contract points:

* **Reading settings:** `getSettings(bindingId)` returns the binding-specific persistent setting; when the user changes the setting, `action.settingsChanged` fires and you redraw.
* **Network:** `networkFetch({ url, method })` is validated by the host (HTTPS-only, host allowlist, bounded request/response). The response body is the bounded value returned by the host adapter; read it defensively.
* **Errors:** on failure, `showError` + a short title; do not swallow exceptions, log them.

## 3. Try it

1. **Reload** the project from the Developer window (since the manifest changed, remove and select it again if needed).
2. On the **Keys** screen, assign the **Hava** action to a key.
3. Enter **Şehir** (city) in the key's settings panel (e.g. `Istanbul`). The key updates like `Istanbul\n+29°C`.
4. Pressing the key refreshes the data.

## Where to go next

* Cache the temperature with `storageSet` and show it instantly on `willAppear` (see <a href="/en/sdk/events-reference">reference</a>: `storage.private`).
* Knob support: switch between cities with `controllers: ["Knob"]` + `action.dialRotate`.
* APIs with credentials: <a href="/en/sdk/capabilities">`network.fetchWithCredential`</a> — the raw token never returns to the plugin.

<Note>This project can be moved to the T2 `sandbox-js` target: the same SDK surface works with the Worker transport. For the public upload status, see <a href="/en/sdk/distribution-status">Distribution status</a>.</Note>
