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

# Automate GitHub from an agent or a pipeline

> File issues, comment on pull requests, and route repository events through Engini tools.

<Info>**Uses:** Python · TypeScript · CLI · REST API</Info>

Two shapes here, and they need different tools. A **pipeline** posting a deterministic comment wants a plain tool call. An **agent** triaging an issue wants a scoped toolset and a model. Both avoid handling a GitHub token in your code.

<Note>
  cURL samples assume `BASE=https://api.engini.io/v1` and `AUTH="x-api-key: $ENGINI_API_KEY"` - the setup from the [REST walkthrough](/examples/rest-walkthrough).
</Note>

## Connect and discover

```bash theme={null}
engini connect github
engini tools list --application github | jq -r '.[] | "\(.tool_slug)\t\(.description)"'
```

Slugs vary by connector version, so capture the ones you need rather than copying them from a doc:

```bash theme={null}
ISSUE_TOOL=$(engini tools list --application github --json \
  | jq -r '.[] | select(.tool_slug | test("issue")) | .tool_slug' | head -1)
engini tools get "$ISSUE_TOOL" | jq '.input_schema'
```

## Shape 1 - deterministic, from CI

No model involved. Read the schema, send the fields:

<CodeGroup>
  ```python Python theme={null}
  result = client.tools.execute(ISSUE_TOOL, {
      "title": "Nightly sync failed",
      "body": f"Job {run_id} failed. Logs: {run_url}",
      "repository": "acme/platform",
  })
  ```

  ```typescript TypeScript theme={null}
  const result = await client.tools.execute(issueTool, {
    title: "Nightly sync failed",
    body: `Job ${runId} failed. Logs: ${runUrl}`,
    repository: "acme/platform",
  });
  ```

  ```bash CLI theme={null}
  engini tools call "$ISSUE_TOOL" --args "$(jq -n \
    --arg title "Nightly sync failed" \
    --arg body  "Job $GITHUB_RUN_ID failed. Logs: $RUN_URL" \
    '{title:$title, body:$body, repository:"acme/platform"}')"
  ```

  ```bash cURL theme={null}
  curl -s -X POST "$BASE/tools/$ISSUE_TOOL/execute" \
    -H "x-api-key: $ENGINI_API_KEY" -H "Content-Type: application/json" \
    -d '{"fields":{"title":"Nightly sync failed","body":"Job failed.","repository":"acme/platform"}}' \
    | jq 'if .isSuccess then .output else {errorMessage, executionInfo} end'
  ```
</CodeGroup>

Validate first on a pull request so a malformed payload fails review rather than production:

```bash theme={null}
engini tools call "$ISSUE_TOOL" --args @payload.json --dry-run
```

Full pipeline patterns - headless auth, exit-code branching - are in [Run Engini in CI/CD](/examples/ci-automation).

## Shape 2 - an agent that triages

Give a model a **narrow** toolset. Note what's deliberately absent: nothing that can close, merge, or delete. (Agent scoping is SDK work - the loop lives in your code.)

<CodeGroup>
  ```python Python theme={null}
  from engini import Engini
  from engini.providers.anthropic import AnthropicProvider

  client = Engini(provider=AnthropicProvider())
  conn_id = next(c.connection_id for c in client.connections.list(application="github"))

  gh = [t.tool_slug for t in client.tools.get(applications=["github"])]
  readonly_plus_comment = [s for s in gh if any(k in s for k in ("get", "list", "search", "comment"))]

  toolset = client.toolset(connections={"github": conn_id}, tools=readonly_plus_comment)
  ```

  ```typescript TypeScript theme={null}
  import { Engini, AnthropicProvider } from "@engini/sdk";

  const client = new Engini({ provider: new AnthropicProvider() });
  const [{ connectionId }] = await client.connections.list("github");

  const gh = (await client.tools.get({ applications: ["github"] })).map((t) => t.toolSlug);
  const readonlyPlusComment = gh.filter((s) =>
    ["get", "list", "search", "comment"].some((k) => s.includes(k)),
  );

  const toolset = client.toolset({ connections: { github: connectionId }, tools: readonlyPlusComment });
  ```
</CodeGroup>

Then run the standard [agent loop](/examples/monday-agent#3-the-agent-loop) with a prompt like *"Read issue #482, find similar past issues, and post a comment linking them."*

The scoping is the point: even if the model decides the issue should be closed, `403 TOOL_NOT_IN_TOOLSET` stops it. That's a control you can show a reviewer, not a promise about prompt discipline.

## Shape 3 - cross-app, one turn

Bind two connections into one toolset and the agent can act across both:

<CodeGroup>
  ```python Python theme={null}
  toolset = client.toolset(connections={
      "github": gh_conn_id,
      "slack":  slack_conn_id,
  })
  ```

  ```typescript TypeScript theme={null}
  const toolset = client.toolset({
    connections: { github: ghConnId, slack: slackConnId },
  });
  ```
</CodeGroup>

Now *"summarize the open PRs and post it to #eng"* is a single conversation, with no glue code between the two APIs.

## Failure modes worth knowing

| What you see                                        | Usually means                                                 |
| --------------------------------------------------- | ------------------------------------------------------------- |
| `isSuccess: false`, `executionInfo.statusCode: 403` | The GitHub token lacks scope for that repo or action          |
| `isSuccess: false`, `statusCode: 404`               | Repo name wrong, or the token can't see a private repo        |
| `isSuccess: false`, `statusCode: 429`               | GitHub's own rate limit - back off, this isn't Engini's limit |
| `409 NO_DEFAULT_CONNECTION`                         | Multiple GitHub connections, none set as default              |

Reading `executionInfo.statusCode` is what separates "GitHub said no" from "Engini said no" - see [debugging](/examples/debug-failed-execution).
