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

# The Casework sample

> Set up the small Casework service the tutorials use, as a project hosted in ReasonOS.

Casework is a small sample service: a team's case queue with an HTTP API, a
browser page, and RBS build and test targets. [Ten minutes with
Casework](/workflow/casework-walkthrough) and [Getting Work
done](/workflow/getting-work-done) both start from it.

It is synthetic on purpose. The four cases and the date (2026-09-14) are fixed,
so every walkthrough starts from the same place. It has no authentication and
no storage, so don't run it as a real service.

## Set it up

1. In ReasonOS, choose **New project**, then **Empty repository**. Keep it
   private. Its empty page shows the commands for pushing existing code to it.
2. On your own machine, make a new folder and create the nine files listed
   below in it, with exactly these names and contents. Mind the leading dot in
   `.gitignore`.
3. In that folder, make the first commit and push it. Replace the example
   address in the `git remote add` line with your project's own address, as
   its empty page shows it:

   ```bash theme={null}
   git init -b main
   git add .
   git commit -m "Start from the Casework sample"
   git remote add origin ssh://git@reasonos.example.com:2222/casework.git
   git push -u origin main
   ```

   Pushing from your machine uses the SSH key registered under your profile; see
   [Cloning and pushing](/platform/git#cloning-and-pushing).
4. Open the project's workspace and check the baseline in its terminal:

   ```bash theme={null}
   rbs build //:server
   rbs coverage //:test
   ```

   **Expected:** the build succeeds, the tests pass, and line coverage is above
   the sample's 80% floor. The sample has no overdue filter yet; the tutorials
   add it.

A project hosted in ReasonOS is needed for every step of
[Getting Work done](/workflow/getting-work-done), including moving a
workspace and submitting a stack.

## The files

This is the sample as the tutorials are written against it.

### `README.md`

````markdown README.md theme={null}
# Casework tutorial sample, version 1

A read-only queue of four invented cases, fixed to September 14, 2026.
Use it for the Getting Work done tutorial at
https://docs.reasonos.com/workflow/getting-work-done.
The baseline has All, Open and Closed filters; Overdue is the exercise.

Open this directory as its own RBS workspace. Run:

```sh
rbs build //:server
rbs coverage //:test
rbs run //:server
```

The server listens on 127.0.0.1:8077. In a hosted workspace open that
address in Tools → Browser, which uses the authorized node proxy.
All shows 4 cases, Open 3, Closed 1. Tests carry an 80% coverage floor.

Create a private ReasonOS-hosted Empty repository and push this baseline
to main before starting the full tutorial. The sample needs no credentials
or customer data. No model run or deployment is included.

Version 1 source baseline: 3b686847b82c7065a7e9e109f89903e352a5156f.
Go and RBS source files are unchanged; this README is tutorial-specific.
````

### `.gitignore`

```text .gitignore theme={null}
# The rbs cache of a branch node or a local build lives here; never a source.
.rbs/
```

### `go.mod`

```text go.mod theme={null}
module example.com/casework

go 1.24.0
```

### `WORKSPACE.rbs`

```python WORKSPACE.rbs theme={null}
load("@rbs//go/toolchain.rbs", "go_toolchain")
go_toolchain(name = "go", version = "1.24.3")
```

### `BUILD.rbs`

```python BUILD.rbs theme={null}
load("@rbs//go/rules.rbs", "go_binary", "go_test")

go_binary(name = "server", srcs = ["go.mod", "main.go", "cases.go", "web.go"])
go_test(
    name = "test",
    srcs = ["go.mod", "main.go", "cases.go", "web.go", "cases_test.go"],
    min_line_coverage = 80,
)
```

### `cases.go`

```go cases.go theme={null}
package main

import "fmt"

// Case contains synthetic demonstration data, never client records.
type Case struct {
	ID     string `json:"id"`
	Title  string `json:"title"`
	Owner  string `json:"owner"`
	Status string `json:"status"`
	Due    string `json:"due"`
}

const demoToday = "2026-09-14"

func sampleCases() []Case {
	return []Case{
		{"CW-101", "Prepare intake summary", "Maya", "open", "2026-09-12"},
		{"CW-102", "Review document checklist", "Theo", "open", "2026-09-14"},
		{"CW-103", "Schedule case conference", "Maya", "open", "2026-09-18"},
		{"CW-104", "Confirm receipt of records", "Theo", "closed", "2026-09-10"},
	}
}

func selectCases(cases []Case, status string) ([]Case, error) {
	if status != "" && status != "all" && status != "open" && status != "closed" {
		return nil, fmt.Errorf("status must be all, open or closed")
	}
	selected := make([]Case, 0, len(cases))
	for _, c := range cases {
		if status == "" || status == "all" || c.Status == status {
			selected = append(selected, c)
		}
	}
	return selected, nil
}
```

### `main.go`

```go main.go theme={null}
package main

import (
	"encoding/json"
	"log"
	"net/http"
	"os"
	"time"
)

func handler() http.Handler {
	mux := http.NewServeMux()
	mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		_, _ = w.Write([]byte(page))
	})
	mux.HandleFunc("GET /api/cases", func(w http.ResponseWriter, r *http.Request) {
		cases, err := selectCases(sampleCases(), r.URL.Query().Get("status"))
		if err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}
		w.Header().Set("Content-Type", "application/json")
		_ = json.NewEncoder(w).Encode(struct {
			Today string `json:"today"`
			Cases []Case `json:"cases"`
		}{demoToday, cases})
	})
	return mux
}

// listenAddr is where the demo serves: CASEWORK_ADDR when set (whatever it
// names, including every interface), else the loopback default so an
// unconfigured demo binds to this machine only. Separate from main so the
// rule is tested without a socket.
func listenAddr(env string) string {
	if env == "" {
		return "127.0.0.1:8077"
	}
	return env
}

func main() {
	addr := listenAddr(os.Getenv("CASEWORK_ADDR"))
	server := &http.Server{Addr: addr, Handler: handler(), ReadHeaderTimeout: 5 * time.Second}
	log.Printf("Casework demo: http://%s (synthetic data; fixed demo date %s)", addr, demoToday)
	log.Fatal(server.ListenAndServe())
}
```

### `web.go`

```go web.go theme={null}
package main

const page = `<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Casework · RBS demo</title>
<style>
*{box-sizing:border-box}body{margin:0;background:#f4f6f8;color:#203042;font:16px system-ui,sans-serif}main{max-width:980px;margin:56px auto;padding:0 24px}header{display:flex;justify-content:space-between;align-items:center;gap:24px}h1{font-size:38px;margin:8px 0}.eyebrow{color:#476451;font-weight:650;letter-spacing:.08em;font-size:12px}p{color:#5b6876;line-height:1.6}label{display:block;font-weight:600;margin:28px 0 8px}select{padding:10px 16px;border:1px solid #c6d0d9;border-radius:8px;background:white;font:inherit}.list{display:grid;gap:12px;margin-top:24px}.case{background:white;padding:20px;border:1px solid #dfe5eb;border-radius:12px;display:flex;justify-content:space-between;gap:20px}.case h2{font-size:17px;margin:4px 0 12px}.meta{font-size:14px;color:#5b6876}.status{height:fit-content;padding:5px 10px;border-radius:20px;background:#eaf0ed;font-size:13px}.note{margin-top:32px;font-size:13px}#error{color:#9c2929}
</style></head><body><main>
<header><div><div class="eyebrow">RBS GUIDED DEMO</div><h1>Casework</h1><p>A small team's case queue, ready for its next improvement.</p></div></header>
<p id="date">Loading the demonstration date…</p>
<label for="status">Show cases</label><select id="status"><option value="all">All cases</option><option value="open">Open cases</option><option value="closed">Closed cases</option></select>
<p id="error" role="alert"></p><section id="cases" class="list" aria-label="Cases" aria-live="polite"></section>
<p class="note">Synthetic records · Read-only sample · Data resets on every request</p>
<script>
let requestNumber=0;
async function refresh(){
 const request=++requestNumber;
 const error=document.getElementById('error');error.textContent='';
 try{
  const response=await fetch('/api/cases?status='+encodeURIComponent(document.getElementById('status').value));
  if(!response.ok)throw new Error('Could not load cases. Try again.');
  const data=await response.json();if(request!==requestNumber)return;
  document.getElementById('date').textContent='Demo date: '+data.today+' · Dates stay fixed for repeatable walkthroughs.';
  const list=document.getElementById('cases');list.replaceChildren();
  for(const c of data.cases){
   const card=document.createElement('article');card.className='case';
   const detail=document.createElement('div');const id=document.createElement('div');id.className='meta';id.textContent=c.id;
   const title=document.createElement('h2');title.textContent=c.title;
   const meta=document.createElement('div');meta.className='meta';meta.textContent=c.owner+' · Due '+c.due;
   const badge=document.createElement('span');badge.className='status';badge.textContent=c.status;
   detail.append(id,title,meta);card.append(detail,badge);list.append(card);
  }
  if(!data.cases.length)list.textContent='No cases match this filter.';
 }catch(e){if(request===requestNumber){document.getElementById('cases').replaceChildren();error.textContent=e.message;}}
}
document.getElementById('status').addEventListener('change',refresh);refresh();
</script></main></body></html>`
```

### `cases_test.go`

```go cases_test.go theme={null}
package main

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
)

func TestSelectCases(t *testing.T) {
	for _, tc := range []struct {
		status string
		want   int
	}{{"", 4}, {"all", 4}, {"open", 3}, {"closed", 1}} {
		t.Run(tc.status, func(t *testing.T) {
			got, err := selectCases(sampleCases(), tc.status)
			if err != nil || len(got) != tc.want {
				t.Fatalf("filter %q: %v, %v", tc.status, got, err)
			}
			for _, c := range got {
				if tc.status == "open" || tc.status == "closed" {
					if c.Status != tc.status {
						t.Fatalf("wrong case: %+v", c)
					}
				}
			}
		})
	}
	if _, err := selectCases(sampleCases(), "typo"); err == nil {
		t.Fatal("unknown filter accepted")
	}
	empty, err := selectCases(nil, "open")
	if err != nil || empty == nil || len(empty) != 0 {
		t.Fatalf("empty result: %v %v", empty, err)
	}
}

func TestCasesHTTP(t *testing.T) {
	h := handler()
	for _, tc := range []struct {
		path   string
		status int
	}{{"/", 200}, {"/api/cases?status=open", 200}, {"/api/cases?status=typo", 400}, {"/missing", 404}} {
		rec := httptest.NewRecorder()
		h.ServeHTTP(rec, httptest.NewRequest("GET", tc.path, nil))
		if rec.Code != tc.status {
			t.Fatalf("%s: %d", tc.path, rec.Code)
		}
		if tc.path == "/" && !strings.Contains(rec.Body.String(), "Casework") {
			t.Fatal("missing page")
		}
		if tc.path == "/api/cases?status=open" {
			var result struct {
				Today string
				Cases []Case
			}
			if err := json.Unmarshal(rec.Body.Bytes(), &result); err != nil {
				t.Fatal(err)
			}
			if result.Today != demoToday || len(result.Cases) != 3 {
				t.Fatalf("unexpected response: %+v", result)
			}
		}
	}
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/api/cases", nil))
	if rec.Code != http.StatusMethodNotAllowed {
		t.Fatalf("write accepted: %d", rec.Code)
	}
}

func TestListenAddrDefaultsToLoopback(t *testing.T) {
	if got := listenAddr(""); got != "127.0.0.1:8077" {
		t.Fatalf("default address = %q, want the loopback default", got)
	}
	if got := listenAddr("0.0.0.0:9000"); got != "0.0.0.0:9000" {
		t.Fatalf("CASEWORK_ADDR was not honoured: %q", got)
	}
}
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.