# Update and move a ticket

> Change a ticket's title, priority, description and other fields, or move it to another column.

Use this to keep a ticket in step with the world outside By Dawn: raise its priority when an alert
gets worse, add what you've learned to its description, or move it along the board. By the end
the ticket will show your changes, and nothing else about it will have changed.

**Before you start:** you need a **Board — read & write** key (see
[Scopes](https://docs.bydawn.com/developers/authentication/#scopes)) and the ticket's `id`, for example from
[Create a ticket](https://docs.bydawn.com/developers/recipes/create-ticket/). Pass the ID as the recipe's argument.

### Swap your key for an access token

Every recipe starts this way, as the [Quickstart](https://docs.bydawn.com/developers/quickstart/) explains.

### Read the ticket as it is now

The recipe reads the ticket first, so it can show what changed, and so it can add to the
description rather than replace it.

### Send only what you want to change

`PATCH /tickets/{ticketId}` changes only the fields you send, and leaves the rest alone. So you
don't need to read a ticket before every update, only when the new value depends on the old one,
as the description does here.

The whole recipe:

**TypeScript** (`update-ticket.ts`):

```ts
// Update a ticket's fields. Only the fields you send change.
// Built-in `fetch` only. The types come from `saw-api.d.ts`, which you generate from the public
// spec (see the Quickstart); without TypeScript, drop the type annotations.
//
//   SAW_API_KEY=saw_live_… node update-ticket.ts <ticketId>
import type { components } from './saw-api';

type Schemas = components['schemas'];
type ErrorSlug = Schemas['ApiErrorSlug'] | Schemas['AccountsApiErrorSlug'];
type Envelope<T> = { ok: true; data: T } | Schemas['ApiError'] | Schemas['AccountsApiError'];

const apiKey = process.env.SAW_API_KEY;
if (!apiKey) throw new Error('Set SAW_API_KEY in the environment.');
const ACCOUNTS_URL = process.env.SAW_ACCOUNTS_URL ?? 'https://accounts.dev.sawrun.com/api/v1';
const WORKSPACE_URL = process.env.SAW_WORKSPACE_URL ?? 'https://workspace.dev.sawrun.com/api/v1';

/** A failed call. Branch on `error`, the stable slug, never on the message. */
class ApiError extends Error {
  status: number;
  error: ErrorSlug;
  constructor(status: number, error: ErrorSlug, message?: string) {
    super(`${status} ${error}${message ? ` — ${message}` : ''}`);
    this.status = status;
    this.error = error;
  }
}

// Sends a request and returns the envelope's `data`. Every response is {"ok": true, "data": …}
// or {"ok": false, "error": "<slug>", "message": …}; a failure throws an ApiError.
async function call<T>(url: string, { method = 'GET', body, token }: { method?: string; body?: unknown; token?: string } = {}): Promise<T> {
  const res = await fetch(url, {
    method,
    headers: {
      ...(body === undefined ? {} : { 'Content-Type': 'application/json' }),
      ...(token ? { Authorization: `Bearer ${token}` } : {}),
    },
    body: body === undefined ? undefined : JSON.stringify(body),
  });
  const envelope = (await res.json()) as Envelope<T>;
  if (envelope.ok) return envelope.data;
  throw new ApiError(res.status, envelope.error, envelope.message);
}

// The access token is a JWT; its `org_id` claim is the organization the API key belongs to.
function orgIdOf(accessToken: string): string {
  return JSON.parse(atob(accessToken.split('.')[1]!.replace(/-/g, '+').replace(/_/g, '/'))).org_id;
}

// 1. Exchange the API key for a short-lived access token. Every call after this goes to your
// organization's URL with the token as a Bearer header.
const exchange: Schemas['ApiKeyExchange'] = { apiKey };
const { accessToken: token } = await call<Schemas['AccessTokenData']['data']>(`${ACCOUNTS_URL}/auth/token/api-key`, { method: 'POST', body: exchange });
const api = `${WORKSPACE_URL}/orgs/${orgIdOf(token)}`;

// 2. Read the ticket you named as it is now, so the output can show what changed.
const ticketId = process.argv[2];
if (!ticketId) throw new Error('Usage: node update-ticket.ts <ticketId>');
const url = `${api}/tickets/${encodeURIComponent(ticketId)}`;

const before = await call<Schemas['Ticket']>(url, { token });

// 3. Send only the fields you want to change.
// Sending `statusId` moves the ticket to another board column. A move into a column with the
// `building` role would start an agent run, so the API refuses it for an API key.
const update: Schemas['TicketUpdate'] = {
  title: 'Updated by the update-ticket recipe',
  priority: 'HIGH',
  description: `${before.description ?? ''}\n\nUpdated through the SAW API.`.trim(),
};
const ticket = await call<Schemas['Ticket']>(url, { method: 'PATCH', body: update, token });

console.log(`${ticket.identifier}: "${before.title}" → "${ticket.title}", priority ${before.priority} → ${ticket.priority}`);
```

Run it:

```sh
SAW_API_KEY=saw_live_… node --experimental-strip-types update-ticket.ts <ticketId>
```

**Python** (`update_ticket.py`):

```python
# Update a ticket's fields. Only the fields you send change.
# Standard library only (urllib and json), Python 3.9 or later.
#
#   SAW_API_KEY=saw_live_… python3 update_ticket.py <ticketId>
import base64
import json
import os
import sys
import urllib.error
import urllib.parse
import urllib.request

API_KEY = os.environ.get("SAW_API_KEY") or sys.exit("Set SAW_API_KEY in the environment.")
ACCOUNTS_URL = os.environ.get("SAW_ACCOUNTS_URL", "https://accounts.dev.sawrun.com/api/v1")
WORKSPACE_URL = os.environ.get("SAW_WORKSPACE_URL", "https://workspace.dev.sawrun.com/api/v1")

class ApiError(Exception):
    """A failed call. Branch on `error`, the stable slug, never on the message."""

    def __init__(self, status, error, message=None):
        super().__init__(f"{status} {error}" + (f" — {message}" if message else ""))
        self.status = status
        self.error = error

def call(url, method="GET", body=None, token=None):
    """Send a request and return the envelope's `data`. Every response is {"ok": true, "data": …}
    or {"ok": false, "error": "<slug>", "message": …}; a failure raises ApiError."""
    headers = {"Content-Type": "application/json"} if body is not None else {}
    if token:
        headers["Authorization"] = f"Bearer {token}"
    data = json.dumps(body).encode() if body is not None else None
    try:
        with urllib.request.urlopen(urllib.request.Request(url, data, headers, method=method)) as response:
            return json.load(response)["data"]
    except urllib.error.HTTPError as failure:
        envelope = json.load(failure)
        raise ApiError(failure.code, envelope["error"], envelope.get("message")) from None

def org_id_of(access_token):
    """The access token is a JWT; its `org_id` claim is the organization the API key belongs to."""
    payload = access_token.split(".")[1]
    return json.loads(base64.urlsafe_b64decode(payload + "=" * (-len(payload) % 4)))["org_id"]

# 1. Exchange the API key for a short-lived access token. Every call after this goes to your
# organization's URL with the token as a Bearer header.
token = call(f"{ACCOUNTS_URL}/auth/token/api-key", "POST", {"apiKey": API_KEY})["accessToken"]
api = f"{WORKSPACE_URL}/orgs/{org_id_of(token)}"

# 2. Read the ticket you named as it is now, so the output can show what changed.
if len(sys.argv) < 2:
    sys.exit("Usage: python3 update_ticket.py <ticketId>")
url = f"{api}/tickets/{urllib.parse.quote(sys.argv[1], safe='')}"

before = call(url, token=token)

# 3. Send only the fields you want to change.
# Sending `statusId` moves the ticket to another board column. A move into a column with the
# `building` role would start an agent run, so the API refuses it for an API key.
ticket = call(
    url,
    "PATCH",
    {
        "title": "Updated by the update-ticket recipe",
        "priority": "HIGH",
        "description": f"{before.get('description') or ''}\n\nUpdated through the SAW API.".strip(),
    },
    token,
)

print(f'{ticket["identifier"]}: "{before["title"]}" → "{ticket["title"]}", priority {before["priority"]} → {ticket["priority"]}')
```

Run it:

```sh
SAW_API_KEY=saw_live_… python3 update_ticket.py <ticketId>
```

**Java** (`UpdateTicket.java`):

```java
// Update a ticket's fields. Only the fields you send change.
// java.net.http for HTTP and Jackson for JSON. JBang runs this file and fetches Jackson; in a
// Maven or Gradle project, add com.fasterxml.jackson.core:jackson-databind and drop the `//` lines.
//
//   SAW_API_KEY=saw_live_… jbang UpdateTicket.java <ticketId>
//JAVA 17+
//COMPILE_OPTIONS -encoding UTF-8
//DEPS com.fasterxml.jackson.core:jackson-databind:2.19.0

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Map;
import java.util.Objects;

public class UpdateTicket {
  /** Every response is an envelope: {"ok": true, "data": …} or {"ok": false, "error": "<slug>", "message": …}. */
  record Envelope<T>(boolean ok, T data, String error, String message) {}
  record AccessToken(String accessToken, int expiresInSeconds) {}
  record Ticket(String id, String identifier, String title, String priority, String description) {}

  /** A failed call. Branch on error(), the stable slug, never on the message. */
  static class ApiException extends RuntimeException {
    final int status;
    final String error;

    ApiException(int status, String error, String message) {
      super(status + " " + error + (message == null ? "" : " — " + message));
      this.status = status;
      this.error = error;
    }
  }

  static final HttpClient http = HttpClient.newHttpClient();
  // Jackson maps the JSON onto the records above and skips the fields they don't name.
  static final ObjectMapper json = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

  public static void main(String[] args) throws Exception {
    // Print → and — as UTF-8, whatever the terminal's locale.
    System.setOut(new PrintStream(new FileOutputStream(FileDescriptor.out), true, StandardCharsets.UTF_8));
    if (args.length < 1) throw new IllegalArgumentException("Usage: jbang UpdateTicket.java <ticketId>");

    String apiKey = System.getenv("SAW_API_KEY");
    if (apiKey == null || apiKey.isEmpty()) throw new IllegalStateException("Set SAW_API_KEY in the environment.");
    String accountsUrl = env("SAW_ACCOUNTS_URL", "https://accounts.dev.sawrun.com/api/v1");
    String workspaceUrl = env("SAW_WORKSPACE_URL", "https://workspace.dev.sawrun.com/api/v1");

    // 1. Exchange the API key for a short-lived access token. Every call after this goes to your
    // organization's URL with the token as a Bearer header.
    String token = call("POST", accountsUrl + "/auth/token/api-key", null, Map.of("apiKey", apiKey), new TypeReference<Envelope<AccessToken>>() {}).accessToken();
    String api = workspaceUrl + "/orgs/" + orgIdOf(token);
    // 2. Read the ticket you named as it is now, so the output can show what changed.
    String url = api + "/tickets/" + URLEncoder.encode(args[0], StandardCharsets.UTF_8);

    Ticket before = call("GET", url, token, null, new TypeReference<Envelope<Ticket>>() {});

    // 3. Send only the fields you want to change.
    // Sending `statusId` moves the ticket to another board column. A move into a column with the
    // `building` role would start an agent run, so the API refuses it for an API key.
    Ticket ticket = call("PATCH", url, token, Map.of(
        "title", "Updated by the update-ticket recipe",
        "priority", "HIGH",
        "description", (Objects.requireNonNullElse(before.description(), "") + "\n\nUpdated through the SAW API.").strip()),
        new TypeReference<Envelope<Ticket>>() {});

    System.out.println(ticket.identifier() + ": \"" + before.title() + "\" → \"" + ticket.title()
        + "\", priority " + before.priority() + " → " + ticket.priority());
  }

  /** Sends a request and returns the envelope's data. A failure throws an ApiException. */
  static <T> T call(String method, String url, String token, Object body, TypeReference<Envelope<T>> type) throws Exception {
    HttpRequest.Builder request = HttpRequest.newBuilder(URI.create(url));
    if (token != null) request.header("Authorization", "Bearer " + token);
    if (body == null) {
      request.method(method, HttpRequest.BodyPublishers.noBody());
    } else {
      request.header("Content-Type", "application/json").method(method, HttpRequest.BodyPublishers.ofString(json.writeValueAsString(body)));
    }
    HttpResponse<String> response = http.send(request.build(), HttpResponse.BodyHandlers.ofString());
    Envelope<T> envelope = json.readValue(response.body(), type);
    if (envelope.ok()) return envelope.data();
    throw new ApiException(response.statusCode(), envelope.error(), envelope.message());
  }

  /** The access token is a JWT; its `org_id` claim is the organization the API key belongs to. */
  static String orgIdOf(String token) throws Exception {
    return json.readTree(Base64.getUrlDecoder().decode(token.split("\\.")[1])).path("org_id").asText();
  }

  static String env(String name, String fallback) {
    String value = System.getenv(name);
    return value == null || value.isEmpty() ? fallback : value;
  }
}
```

Run it:

```sh
SAW_API_KEY=saw_live_… jbang UpdateTicket.java <ticketId>
```

**C#** (`update-ticket.cs`):

```csharp
// Update a ticket's fields. Only the fields you send change.
// Plain HttpClient and System.Net.Http.Json, both part of .NET: no NuGet packages.
//
//   SAW_API_KEY=saw_live_… dotnet run update-ticket.cs -- <ticketId>
//
// `dotnet run <file>.cs` builds for Native AOT by default, which turns off reflection-based JSON;
// the `#:` line below turns AOT off. In a console project, leave that line out and paste the rest
// into Program.cs.
#:property PublishAot=false
using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;

if (args.Length < 1) throw new ArgumentException("Usage: dotnet run update-ticket.cs -- <ticketId>");
var ticketId = Uri.EscapeDataString(args[0]);

var apiKey = Environment.GetEnvironmentVariable("SAW_API_KEY");
if (string.IsNullOrEmpty(apiKey)) throw new InvalidOperationException("Set SAW_API_KEY in the environment.");
var accountsUrl = Environment.GetEnvironmentVariable("SAW_ACCOUNTS_URL") ?? "https://accounts.dev.sawrun.com/api/v1";
var workspaceUrl = Environment.GetEnvironmentVariable("SAW_WORKSPACE_URL") ?? "https://workspace.dev.sawrun.com/api/v1";

// 1. Exchange the API key for a short-lived access token.
using var accounts = new HttpClient();
var token = await ReadAsync<AccessTokenData>(await accounts.PostAsJsonAsync($"{accountsUrl}/auth/token/api-key", new { apiKey }));

// Every call goes to your organization's URL with the token as a Bearer header.
using var http = new HttpClient { BaseAddress = new Uri($"{workspaceUrl}/orgs/{OrgIdOf(token.AccessToken)}/") };
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.AccessToken);

// 2. Read the ticket you named as it is now, so the output can show what changed.
var before = await ReadAsync<Ticket>(await http.GetAsync($"tickets/{ticketId}"));

// 3. Send only the fields you want to change.
// Sending `statusId` moves the ticket to another board column. A move into a column with the
// `building` role would start an agent run, so the API refuses it for an API key.
var ticket = await ReadAsync<Ticket>(await http.PatchAsJsonAsync($"tickets/{ticketId}", new
{
    title = "Updated by the update-ticket recipe",
    priority = "HIGH",
    description = $"{before.Description}\n\nUpdated through the SAW API.".Trim(),
}));

Console.WriteLine($"{ticket.Identifier}: \"{before.Title}\" → \"{ticket.Title}\", priority {before.Priority} → {ticket.Priority}");

// Helpers: the functions and types the steps above use.
// Every response is an envelope: {"ok": true, "data": …} or {"ok": false, "error": "<slug>", "message": …}.
static async Task<T> ReadAsync<T>(HttpResponseMessage response)
{
    var envelope = await response.Content.ReadFromJsonAsync<Envelope<T>>();
    if (envelope is { Ok: true, Data: not null }) return envelope.Data;
    throw new ApiException(response.StatusCode, envelope?.Error ?? "internal_error", envelope?.Message);
}

// The access token is a JWT; its `org_id` claim is the organization the API key belongs to.
static string OrgIdOf(string accessToken)
{
    var payload = accessToken.Split('.')[1].Replace('-', '+').Replace('_', '/');
    var json = Convert.FromBase64String(payload.PadRight(payload.Length + (4 - payload.Length % 4) % 4, '='));
    return JsonDocument.Parse(json).RootElement.GetProperty("org_id").GetString()!;
}

record Envelope<T>(bool Ok, T? Data, string? Error, string? Message);
record AccessTokenData(string AccessToken, int ExpiresInSeconds);
record Ticket(string Id, string Identifier, string Title, string Priority, string? Description);

/// <summary>A failed call. Branch on <see cref="Error"/>, the stable slug, never on the message.</summary>
class ApiException(HttpStatusCode status, string error, string? message)
    : Exception($"{(int)status} {error}{(message is null ? "" : $" — {message}")}")
{
    public HttpStatusCode Status { get; } = status;
    public string Error { get; } = error;
}
```

Run it:

```sh
SAW_API_KEY=saw_live_… dotnet run update-ticket.cs -- <ticketId>
```

**Go** (`update_ticket.go`):

```go
// Update a ticket's fields. Only the fields you send change.
// Standard library only (net/http and encoding/json).
//
//	SAW_API_KEY=saw_live_… go run update_ticket.go <ticketId>
package main

import (
	"bytes"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"strings"
)

// envelope is every response: {"ok": true, "data": …} or {"ok": false, "error": "<slug>", "message": …}.
type envelope[T any] struct {
	OK      bool   `json:"ok"`
	Data    T      `json:"data"`
	Error   string `json:"error"`
	Message string `json:"message"`
}

// apiError is a failed call. Branch on Slug, the stable error slug, never on Message.
type apiError struct {
	Status  int
	Slug    string
	Message string
}

func (e *apiError) Error() string {
	if e.Message == "" {
		return fmt.Sprintf("%d %s", e.Status, e.Slug)
	}
	return fmt.Sprintf("%d %s — %s", e.Status, e.Slug, e.Message)
}

type accessToken struct {
	AccessToken      string `json:"accessToken"`
	ExpiresInSeconds int    `json:"expiresInSeconds"`
}

type ticket struct {
	ID          string  `json:"id"`
	Identifier  string  `json:"identifier"`
	Title       string  `json:"title"`
	Description *string `json:"description"`
	Priority    string  `json:"priority"`
}

func main() {
	apiKey := os.Getenv("SAW_API_KEY")
	if apiKey == "" {
		fail("Set SAW_API_KEY in the environment.")
	}
	accountsURL := env("SAW_ACCOUNTS_URL", "https://accounts.dev.sawrun.com/api/v1")
	workspaceURL := env("SAW_WORKSPACE_URL", "https://workspace.dev.sawrun.com/api/v1")

	// 1. Exchange the API key for a short-lived access token. Every call after this goes to your
	// organization's URL with the token as a Bearer header.
	exchange, err := call[accessToken](http.MethodPost, accountsURL+"/auth/token/api-key", "", map[string]string{"apiKey": apiKey})
	check(err)
	token := exchange.AccessToken
	api := workspaceURL + "/orgs/" + orgIDOf(token)

	// 2. Read the ticket you named as it is now, so the output can show what changed.
	if len(os.Args) < 2 {
		fail("Usage: go run update_ticket.go <ticketId>")
	}
	ticketURL := api + "/tickets/" + url.PathEscape(os.Args[1])

	before, err := call[ticket](http.MethodGet, ticketURL, token, nil)
	check(err)

	description := ""
	if before.Description != nil {
		description = *before.Description
	}
	// 3. Send only the fields you want to change.
	// Sending `statusId` moves the ticket to another board column. A move into a column with the
	// `building` role would start an agent run, so the API refuses it for an API key.
	updated, err := call[ticket](http.MethodPatch, ticketURL, token, map[string]string{
		"title":       "Updated by the update-ticket recipe",
		"priority":    "HIGH",
		"description": strings.TrimSpace(description + "\n\nUpdated through the SAW API."),
	})
	check(err)

	fmt.Printf("%s: \"%s\" → \"%s\", priority %s → %s\n", updated.Identifier, before.Title, updated.Title, before.Priority, updated.Priority)
}

// call sends a request and decodes the envelope's data into T. A failure is an *apiError.
func call[T any](method, url, token string, body any) (T, error) {
	var data T
	var reader io.Reader
	if body != nil {
		encoded, err := json.Marshal(body)
		if err != nil {
			return data, err
		}
		reader = bytes.NewReader(encoded)
	}
	req, err := http.NewRequest(method, url, reader)
	if err != nil {
		return data, err
	}
	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}
	if token != "" {
		req.Header.Set("Authorization", "Bearer "+token)
	}
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		return data, err
	}
	defer res.Body.Close()
	var out envelope[T]
	if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
		return data, fmt.Errorf("%s %s answered %s with no JSON envelope", method, url, res.Status)
	}
	if !out.OK {
		return data, &apiError{Status: res.StatusCode, Slug: out.Error, Message: out.Message}
	}
	return out.Data, nil
}

// orgIDOf reads the organization the API key belongs to: the access token is a JWT, and its
// `org_id` claim names it.
func orgIDOf(token string) string {
	parts := strings.Split(token, ".")
	if len(parts) < 2 {
		fail("The access token is not a JWT.")
	}
	payload, err := base64.RawURLEncoding.DecodeString(parts[1])
	check(err)
	var claims struct {
		OrgID string `json:"org_id"`
	}
	check(json.Unmarshal(payload, &claims))
	return claims.OrgID
}

func env(name, fallback string) string {
	if value := os.Getenv(name); value != "" {
		return value
	}
	return fallback
}

func check(err error) {
	if err != nil {
		fail(err.Error())
	}
}

func fail(message string) {
	fmt.Fprintln(os.Stderr, message)
	os.Exit(1)
}
```

Run it:

```sh
SAW_API_KEY=saw_live_… go run update_ticket.go <ticketId>
```

**cURL** (`update-ticket.sh`):

```bash
#!/usr/bin/env bash
# Update a ticket's fields. Only the fields you send change.
# Needs curl and jq: jq builds the request bodies and reads the responses.
#
#   SAW_API_KEY=saw_live_… bash update-ticket.sh <ticketId>
set -euo pipefail

ticket_id=${1:?Usage: bash update-ticket.sh <ticketId>}

: "${SAW_API_KEY:?Set SAW_API_KEY in the environment.}"
ACCOUNTS_URL="${SAW_ACCOUNTS_URL:-https://accounts.dev.sawrun.com/api/v1}"
WORKSPACE_URL="${SAW_WORKSPACE_URL:-https://workspace.dev.sawrun.com/api/v1}"

# Every response is {"ok": true, "data": …} or {"ok": false, "error": "<slug>", "message": …}.
# Piped through this jq filter, a success prints its `data`; a failure prints the envelope on
# stderr and stops the script.
DATA='if .ok then .data else halt_error end'

# 1. Exchange the API key for a short-lived access token. jq reads the key from the environment, so
# it never appears on a command line.
token=$(jq -n '{apiKey: env.SAW_API_KEY}' |
  curl -sS "$ACCOUNTS_URL/auth/token/api-key" -H 'Content-Type: application/json' --data @- |
  jq -r "$DATA | .accessToken")

# The token is a JWT; its `org_id` claim is the organization the key belongs to. Every call
# after this goes to that organization's URL with the token as a Bearer header.
org_id=$(jq -rR 'split(".")[1] | gsub("-"; "+") | gsub("_"; "/") | . + ("=" * ((4 - length % 4) % 4))
  | @base64d | fromjson | .org_id' <<<"$token")
api="$WORKSPACE_URL/orgs/$org_id"
# 2. Read the ticket you named as it is now, so the output can show what changed.
url="$api/tickets/$(jq -rn --arg id "$ticket_id" '$id | @uri')"

before=$(curl -sS "$url" -H "Authorization: Bearer $token" | jq "$DATA")

# 3. Send only the fields you want to change.
# Sending `statusId` moves the ticket to another board column. A move into a column with the
# `building` role would start an agent run, so the API refuses it for an API key.
ticket=$(jq '{
    title: "Updated by the update-ticket recipe",
    priority: "HIGH",
    description: ((.description // "") + "\n\nUpdated through the SAW API." | sub("^\\s+"; "") | sub("\\s+$"; ""))
  }' <<<"$before" |
  curl -sS -X PATCH "$url" -H "Authorization: Bearer $token" -H 'Content-Type: application/json' --data @- |
  jq "$DATA")

jq -rn --argjson before "$before" --argjson ticket "$ticket" \
  '"\($ticket.identifier): \"\($before.title)\" → \"\($ticket.title)\", priority \($before.priority) → \($ticket.priority)"'
```

Run it:

```sh
SAW_API_KEY=saw_live_… bash update-ticket.sh <ticketId>
```

What you'll see (from our test organization; your names and IDs will differ):

```text
WEB-5: "Created by the create-ticket recipe" → "Updated by the update-ticket recipe", priority LOW → HIGH
```

## Run it in your browser

Run the TypeScript recipe from this page. It changes the ticket's title, priority and description.
A ticket you [created here](https://docs.bydawn.com/developers/recipes/create-ticket/) is filled in for you. See
[Try it in your browser](https://docs.bydawn.com/developers/try-it/).

> A runner for this recipe, with your own credential, is on the [web version of this page](https://docs.bydawn.com/developers/recipes/update-ticket/).

## Things to know

- **Lists are replaced, not merged.** `labelIds` and `repoIds` replace the whole list, so to add
  a label, send the IDs it already has plus the new one.
- **Leave out, `null` or a value.** For fields like `cycleId` and `estimate`, leaving the field
  out keeps it, `null` clears it, and a value sets it.
- **Moving between columns.** Send a new `statusId` to move the ticket. The one move a key can't
  make is into the column that starts By Dawn's agent (see
  [what a key can't do](https://docs.bydawn.com/developers/authentication/#what-a-key-cant-do)).
- **Archiving.** `POST /tickets/{ticketId}/archive` takes a ticket off the board, and
  `/unarchive` brings it back.

Source: https://docs.bydawn.com/developers/recipes/update-ticket/
