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

# Use Office Scripts and Excel to create custom reports

> Learn how to use Office Scripts and the Manifest API to create custom Excel reports directly from Excel for the web.

# Exporting Manifest Faults to Excel with Office Scripts

This article walks through `manifest_fault_excel_automate.ts`, an Office Script that pulls fault records from the **Taqtile Manifest API** and writes them directly into the active Excel worksheet. It also explains how you can adapt the script to build other reports against the same API.

Unlike the Python equivalent, this script runs entirely inside **Excel for the web** through the **Automate** tab — no local Python installation, no `pip install`, and no separate `.xlsx` output file. The workbook you are looking at *is* the report.

***

## What the Script Does

At a high level, the script:

1. Authenticates with the Manifest REST API to get a JWT token
2. Sends a single GraphQL query to fetch the first 100 faults for the configured organization
3. Writes a header row and one row per fault to the active worksheet

The output appears immediately in the worksheet you ran the script from, starting at cell `A1`.

***

manifest\_fault\_excel\_automate.ts file:

```typescript expandable theme={null}
interface LoginResponse {
    user: {
        token: string;
    };
}

interface Asset {
    id: number;
    serialNumber: string;
}

interface Fault {
    id: number;
    description: string;
    createdByUserId: number;
    resolvedByUserId: number | null;
    createdAt: string;
    resolvedAt: string | null;
    asset: Asset | null;
}

interface GraphQLResponse {
    data: {
        getFaults: Fault[];
    };
}

async function main(workbook: ExcelScript.Workbook): Promise<void> {
    // Step 1: Login
    const loginRes = await fetch("https://[domain].taqmanifest.com/rest/signin/v3", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ email: "username@domain.com", password: "password" })
    });

    if (!loginRes.ok) {
        const errorText: string = await loginRes.text();
        throw new Error(`Login failed (${loginRes.status}): ${errorText}`);
    }

    const loginData: LoginResponse = await loginRes.json() as LoginResponse;
    const token: string = loginData.user.token;

    if (!token) {
        throw new Error(`Login succeeded but no token found. Response: ${JSON.stringify(loginData)}`);
    }

    // Step 2: GraphQL query
    const gqlRes = await fetch("https://[domain].taqmanifest.com/graphql/v3", {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
            "Authorization": token
        },
        body: JSON.stringify({
            query: "{ getFaults(itemsPerPage: 100, pageNumber: 1, orgId: [1]) { id description createdByUserId resolvedByUserId createdAt resolvedAt asset { id serialNumber } } }"
        })
    });

    if (!gqlRes.ok) {
        const errorText: string = await gqlRes.text();
        throw new Error(`GraphQL failed (${gqlRes.status}): ${errorText}`);
    }

    const gqlData: GraphQLResponse = await gqlRes.json() as GraphQLResponse;
    const faults: Fault[] = gqlData.data.getFaults;

    // Step 3: Write to sheet
    const sheet: ExcelScript.Worksheet = workbook.getActiveWorksheet();
    sheet.getRange("A1:H1").setValues([["ID", "Description", "Created By", "Resolved By", "Created At", "Resolved At", "Asset ID", "Asset Serial"]]);

    faults.forEach((fault: Fault, i: number) => {
        sheet.getRange(`A${i + 2}:H${i + 2}`).setValues([[
            fault.id,
            fault.description ?? "",
            fault.createdByUserId ?? "",
            fault.resolvedByUserId ?? "",
            fault.createdAt ?? "",
            fault.resolvedAt ?? "",
            fault.asset ? fault.asset.id : "",
            fault.asset ? fault.asset.serialNumber : ""
        ]]);
    });
}
```

## Requirements

* **Excel for the web** with an Microsoft 365 (Business or Enterprise) license that includes the **Automate** tab. Office Scripts are not available in the desktop or mobile versions of Excel.
* A **Manifest user account** with permission to read faults for the target organization.
* Your Manifest **subdomain**, **email**, **password**, and target **organization ID**.

No additional packages or installations are required — Office Scripts run in a sandboxed TypeScript runtime hosted by Microsoft.

***

## Usage

1. Open the workbook you want the report written to in **Excel for the web**.
2. Select the **Automate** tab in the ribbon, then choose **New Script**.
3. Paste the contents of `manifest_fault_excel_automate.ts` into the Code Editor pane, replacing the default `main` stub.
4. Edit the three placeholders near the top of the script:
   * `[domain]` — your Manifest subdomain (used in both the REST and GraphQL URLs)
   * `username@domain.com` — your Manifest login email
   * `password` — your Manifest password
5. If your data lives under a different organization, update `orgId: [1]` in the GraphQL query to the correct ID.
6. Click **Save script**, give it a name (for example, *Manifest Faults Export*), then click **Run**.

The first time you run the script, Excel will prompt you to allow it to make external network requests. You must approve this for the API calls to succeed.

When the run finishes, the active worksheet will contain a header row in row 1 and one fault per row starting at row 2.

***

## How It Works, Step by Step

### 1. Authentication

The script POSTs credentials to the Manifest REST endpoint:

```
POST /rest/signin/v3
Content-Type: application/json

{ "email": "...", "password": "..." }
```

The response JSON contains `user.token`, which is a JWT used as the `Authorization` header on the GraphQL request that follows. If the login call fails, or the response succeeds but contains no token, the script throws and Excel surfaces the error in the run pane.

### 2. Fetching Faults

A single GraphQL query is sent to the Manifest GraphQL endpoint:

```graphql theme={null}
{
  getFaults(itemsPerPage: 100, pageNumber: 1, orgId: [1]) {
    id
    description
    createdByUserId
    resolvedByUserId
    createdAt
    resolvedAt
    asset {
      id
      serialNumber
    }
  }
}
```

The query is intentionally compact — it requests the first 100 faults for organization `1` and returns only the fields the report needs. There is no pagination loop in this script: if you have more than 100 faults and need them all, see *Adapting the Script* below.

> **Note on the Authorization header:** Office Scripts use the standard browser `fetch` API, and the token is passed directly as the `Authorization` header value (no `Bearer` prefix), matching the convention used by the Manifest API.

### 3. Writing to the Worksheet

The script writes results to the **active worksheet** — whichever sheet is selected when you press **Run**. It uses two `Range` operations:

* A single `setValues` call writes the header row to `A1:H1`.
* A `forEach` over the faults writes each row individually with `setValues` on `A{n}:H{n}`.

The columns are:

| Column       | Source                     |
| ------------ | -------------------------- |
| ID           | `fault.id`                 |
| Description  | `fault.description`        |
| Created By   | `fault.createdByUserId`    |
| Resolved By  | `fault.resolvedByUserId`   |
| Created At   | `fault.createdAt`          |
| Resolved At  | `fault.resolvedAt`         |
| Asset ID     | `fault.asset.id`           |
| Asset Serial | `fault.asset.serialNumber` |

`null` values are coerced to empty strings using the `??` operator so the cells render blank rather than showing `null`. Faults with no associated asset write blank cells in the last two columns.

> **Note on dates:** `createdAt` and `resolvedAt` are written exactly as the API returns them (ISO 8601 strings). Excel will display them as text. To convert them into native Excel datetimes, use a formula in an adjacent column such as `=DATEVALUE(LEFT(E2,10))+TIMEVALUE(MID(E2,12,8))`, or modify the script to parse and write a `Date` object.

***

## Adapting the Script for Other Reports

The script is structured in three clearly separated phases — auth, query, write — making it straightforward to swap in different queries or output shapes.

### Change the GraphQL Query

The query is a plain string passed in the `body` of the `gqlRes` `fetch` call. To report on a different resource, replace the query string and update the data extraction path and `Fault` interface to match the new shape:

```typescript theme={null}
// Example: query assets instead of faults
interface AssetRecord {
    id: number;
    name: string;
    assetClass: { title: string } | null;
}

interface AssetResponse {
    data: { assets: AssetRecord[] };
}

const gqlRes = await fetch("https://[domain].taqmanifest.com/graphql/v3", {
    method: "POST",
    headers: { "Content-Type": "application/json", "Authorization": token },
    body: JSON.stringify({
        query: "{ assets(itemsPerPage: 100, pageNumber: 1) { id name assetClass { title } } }"
    })
});

const gqlData: AssetResponse = await gqlRes.json() as AssetResponse;
const records: AssetRecord[] = gqlData.data.assets;
```

### Add or Remove Columns

Two places control the columns: the header row written to `A1:H1`, and the row written inside `forEach`. To add a column, extend both arrays in the same order, and widen the range from `H` to `I` (or further). Keeping the header and the data row in sync is the only constraint.

### Paginate Through All Results

The script fetches only the first page. To pull every fault, wrap the GraphQL call in a loop and increment `pageNumber` until a page returns fewer than `itemsPerPage` records:

```typescript theme={null}
const allFaults: Fault[] = [];
let page = 1;
const PAGE_SIZE = 100;

while (true) {
    const gqlRes = await fetch("https://[domain].taqmanifest.com/graphql/v3", {
        method: "POST",
        headers: { "Content-Type": "application/json", "Authorization": token },
        body: JSON.stringify({
            query: `{ getFaults(itemsPerPage: ${PAGE_SIZE}, pageNumber: ${page}, orgId: [1]) { id description createdByUserId resolvedByUserId createdAt resolvedAt asset { id serialNumber } } }`
        })
    });
    const gqlData: GraphQLResponse = await gqlRes.json() as GraphQLResponse;
    const batch = gqlData.data.getFaults;
    allFaults.push(...batch);
    if (batch.length < PAGE_SIZE) break;
    page += 1;
}
```

### Filter Client-Side

Once the data is in memory, filter with standard `Array.prototype.filter` calls before writing. For example, to show only unresolved faults:

```typescript theme={null}
const openFaults: Fault[] = faults.filter(f => f.resolvedAt === null);
```

### Write Faster with a Single `setValues` Call

The current script writes one row at a time, which is the slowest pattern in Office Scripts. For larger result sets, build a 2-D array first and write it all at once:

```typescript theme={null}
const rows: (string | number)[][] = faults.map(f => [
    f.id,
    f.description ?? "",
    f.createdByUserId ?? "",
    f.resolvedByUserId ?? "",
    f.createdAt ?? "",
    f.resolvedAt ?? "",
    f.asset ? f.asset.id : "",
    f.asset ? f.asset.serialNumber : ""
]);
sheet.getRange(`A2:H${rows.length + 1}`).setValues(rows);
```

This is dramatically faster for hundreds or thousands of rows.

### Format the Output

Office Scripts can also style the cells they write. For example, bolding the header row and auto-fitting the columns:

```typescript theme={null}
const header = sheet.getRange("A1:H1");
header.getFormat().getFont().setBold(true);
sheet.getRange("A:H").getFormat().autofitColumns();
```

### Point at a Different Environment

The two `fetch` URLs control the target environment. Change them to switch between staging and production:

```typescript theme={null}
const BASE = "https://app.taqmanifest.com";       // production
// const BASE = "https://test.taqmanifest.com";   // staging
```

Then build the REST and GraphQL URLs from the constant. Office Scripts admins may also need to allow the destination domain in the Microsoft 365 admin center if external API access is restricted.

***

## Security Note

The script currently has credentials embedded as string literals inside `main`. Office Scripts do not have access to environment variables, OS-level secret stores, or the Excel Identity APIs that desktop add-ins use, so true secret management is not available in this runtime.

For shared or recurring use, prefer one of these approaches:

* **Pass credentials as parameters** when running through **Power Automate**. Office Scripts called from a flow can accept arguments, letting you store the email and password in a secured Power Automate connection or environment variable rather than in the script body.
* **Restrict the script's audience** by saving it to a personal OneDrive rather than a shared SharePoint library.
* **Use a service account** with the minimum permissions required to read the data the report needs, rather than a privileged user account.

Avoid sharing the script file to a public location while it still contains real credentials.

***

## Summary

`manifest_fault_excel_automate.ts` demonstrates the typical pattern for building Manifest reports inside Excel for the web:

1. Authenticate once via REST to get a token
2. Run a GraphQL query for the resource you want
3. Write the result directly into the active worksheet with `Range.setValues`

Because each phase is independent, swapping in a different query, adding pagination, or reformatting the output requires changes in only one or two places — making it a good starting template for any report you want to build against the Manifest API without leaving the browser.
