# SpaceGass API
> Complete documentation for Large Language Models
---
## Document: API Versioning
How API versioning works in the SpaceGass API
URL: /docs/versioning
# API Versioning
The SpaceGass API uses URL segment versioning. The version number
is part of the URL path.
## URL Format
```
http://your-host/api/v{version}/{resource}
```
All current endpoints use **v1**:
```
http://localhost:34560/api/v1/job
http://localhost:34560/api/v1/job/structure/nodes
http://localhost:34560/api/v1/job/structure/members
```
## SDK Clients
The generated SDK clients have the version baked into the base URL,
so you don't need to manage it:
```csharp title="C#"
// The base URL includes /api/v1 — just navigate from the client
var nodes = await client.Job.Structure.Nodes.GetAsync();
```
```python title="Python"
# Same in Python — the base URL handles versioning
nodes = await client.job.structure.nodes.get()
```
## Package Versioning
SDK and package versions mirror the SPACE GASS Desktop version to
eliminate ambiguity:
| SPACE GASS Desktop | NuGet Package | PyPI Package |
|---|---|---|
| v15.0.2 | `SpaceGassApi 15.0.2` | `space-gass-api==15.0.2` |
| v15.1.0 | `SpaceGassApi 15.1.0` | `space-gass-api==15.1.0` |
---
## Document: Using the SDK
A plain-English reference for the REST verbs and async patterns used by the SPACE GASS SDK
URL: /docs/using-the-sdk
# Using the SDK
> **Want to generate your own client?** These SDKs are generated from the
> OpenAPI spec at
> **https://api.spacegass.com/docs/api/1/schema.json** — feed that file
> to [Kiota](https://learn.microsoft.com/en-us/openapi/kiota/overview),
> OpenAPI Generator, or an LLM to produce a typed client in any language.
If you are new to web APIs, the SDK calls and HTTP examples in this
documentation can feel jargon-heavy. This page is a quick reference for
the terms you will see repeatedly — what they actually mean, and how
they map to engineering terms you already know.
## The REST verbs
The API uses standard HTTP verbs. Each one has a consistent meaning:
| Verb | What it means in plain English | SDK call ends in… |
| -------- | -------------------------------------------------------------------- | ----------------- |
| `GET` | **Read** something — fetch a value without changing anything | `GetAsync()` |
| `POST` | **Create** a new thing — add a node, member, load case, etc. | `PostAsync(...)` |
| `PATCH` | **Update** an existing thing in place — change one or two properties | `PatchAsync(...)` |
| `PUT` | **Replace** an existing thing wholesale — replace its entire state | `PutAsync(...)` |
| `DELETE` | **Remove** an existing thing | `DeleteAsync()` |
So `client.Job.Structure.Nodes.PostAsync(...)` reads as "post (create) a
new node", and `client.Job.Structure.Nodes[5].DeleteAsync()` reads as
"delete the node with Id 5".
## The `Async` suffix and `await`
Every SDK call ends in `Async` (in C#) or is awaited (in Python):
```csharp title="C#"
var node = await client.Job.Structure.Nodes.PostAsync(new NodeCreate { ... });
```
```python title="Python"
node = await client.job.structure.nodes.post(NodeCreate(...))
```
The `Async` / `await` machinery is there because the API is reached over
the network. Your code does not freeze the program while it waits for a
response — it lets the rest of your application keep working until the
result comes back. For day-to-day use you can read `await foo` as
"do `foo` and give me the result"; the rest is plumbing.
In C#, the `Async` suffix is a naming convention — every method that
returns a `Task` is suffixed `Async` so it is obvious from the method
name that you must `await` it.
## Request and response bodies
Most `POST`, `PATCH`, and `PUT` calls take a **body** — the data you
are sending up. The SDK exposes this as a strongly-typed object:
- `NodeCreate` — the shape of the body when creating a node
- `MemberDistributedLoadCreate` — the body when creating a distributed load
- `SectionUpdate` — the body when patching a section's properties
A `*Create` model has all the fields you can set on creation; a
`*Update` model is the same shape but every field is optional, so you
only need to set the ones you want to change.
## Path parameters and query parameters
When you see `/job/structure/nodes/{id}`, the `{id}` is a **path
parameter** — replaced with the actual value you want, e.g. `5`. The
SDK exposes this as a typed indexer:
```csharp title="C#"
await client.Job.Structure.Nodes[5].GetAsync();
```
```python title="Python"
await client.job.structure.nodes.by_id(5).get()
```
Anything after a `?` in a URL is a **query parameter** — used for
filtering, pagination, and similar options. The SDK exposes them as
typed properties on a configuration object:
```csharp title="C#"
await client.Job.Query.Analysis.Static.NodeReactions.GetAsync(config =>
{
config.QueryParameters.LoadCases = "1,3-7";
config.QueryParameters.Nodes = "10-12";
});
```
```python title="Python"
await client.job.query.analysis.static.node_reactions.get(
load_cases="1,3-7", nodes="10-12")
```
```bash title="curl"
curl "http://localhost:34560/api/v1/job/query/analysis/static/node-reactions?loadCases=1,3-7&nodes=10-12"
```
See the [Filtering & Querying guide](/guides/filtering-and-querying) for
the full SG list-string filter syntax.
## Status codes
Every response carries an HTTP status code. The ones you will see in
practice:
| Code | Meaning |
| ---- | ----------------------------------------------------------------------------- |
| 200 | OK — your read worked |
| 201 | Created — your new entity was saved |
| 204 | No Content — your delete or update worked, no data to return |
| 400 | Bad Request — the body you sent was invalid; check the `detail` field |
| 404 | Not Found — the entity (or file) does not exist |
| 409 | Conflict — the operation clashes with current state (e.g. file already open) |
| 500 | Server Error — the API ran into trouble; please report it via [Support](/support) |
The SDK clients turn non-2xx responses into typed exceptions
(`ApiException` in C#, `APIError` in Python). See
[Error Handling](/guides/error-handling) for the patterns.
---
## Document: Support
Get help with the SPACE GASS API
URL: /docs/support
# Support
Need help with the SPACE GASS API? Reach the team directly:
**Email:** [support@spacegass.com](mailto:support@spacegass.com)
When reporting an issue, please include:
- Your SPACE GASS Desktop version (visible at the top of the SPACE GASS window, or via `GET /service/info`)
- The endpoint you were calling and the request body, if applicable
- The full error response (status code, `title`, `detail`, and `traceId`)
- A short description of what you expected to happen
---
## Document: Quick Start
Get started with the SpaceGass API in 5 minutes
URL: /docs/quick-start
# Quick Start
This guide walks you through connecting to the API, opening a built-in
sample project, and reading nodes — using the SDK clients or plain
HTTP. No project file of your own is required.
### Prerequisites
- [SPACE GASS](https://www.spacegass.com/) **14.5 or later** installed on your machine
- SPACE GASS has been opened at least once to initialise the required
data files
- One of:
- **C#** — [.NET 8 SDK](https://dotnet.microsoft.com/download) (any
editor works; [Visual Studio Code](https://code.visualstudio.com/)
+ the C# Dev Kit extension is a free option), or
- **Python** — [Python 3.10 or newer](https://www.python.org/downloads/)
(any editor works; VS Code + the Python extension is a free option), or
- **Just an HTTP client** — `curl` is built into Windows 10+;
[Postman](https://www.postman.com/) and [Insomnia](https://insomnia.rest/)
also work.
1. **Start the Service**
The API runs as a local server. The easiest way to start it is to
double-click the **SPACE GASS API** shortcut under the SPACE GASS
Windows application folder.
Alternatively, open a terminal and run:
```bash
"C:\Program Files\SPACE GASS 14.5\SpaceGassApi.exe"
```
By default the service starts on `http://localhost:34560`. To use a
different port, pass the `--port` flag:
```bash
"C:\Program Files\SPACE GASS 14.5\SpaceGassApi.exe" --port=5025
```
Leave the terminal or shortcut window open — the service runs until
you close it.
1. **Explore the API**
Once the service is running, open your browser and navigate to:
```
http://localhost:34560/swagger
```
The interactive Swagger UI lets you browse every endpoint, see
request and response schemas, and make live API calls directly
from your browser — no SDK or code required. This is the quickest
way to get familiar with the API.
When you are ready to integrate the API into your own code,
continue with the steps below.
1. **Create a Project Folder and Install the SDK**
The API includes generated SDK clients for C# and Python. These
handle serialisation and give you typed methods for every endpoint.
If you prefer raw HTTP, you can skip this step and use the **curl**
tab in every snippet below.
```bash title="C#"
# In a fresh folder:
dotnet new console -n SpaceGassQuickStart
cd SpaceGassQuickStart
dotnet add package SpaceGassApi
```
```bash title="Python"
# In a fresh folder:
python -m venv .venv
.venv\Scripts\activate # macOS/Linux: source .venv/bin/activate
pip install space-gass-api
```
```bash title="curl"
# No installation needed — curl is built into Windows 10+ and most
# macOS/Linux installations. Postman or Insomnia work too.
```
1. **Create the Client**
The SDK provides a `CreateClient()` factory that configures
everything for you — it connects to the local API with no
additional setup required. See
[Authentication](/authentication) for details on
custom URLs and future API key support.
```csharp title="C#"
using SpaceGassApi;
using SpaceGassApi.Models;
var client = SpaceGassApiClient.CreateClient("http://localhost:34560");
```
```python title="Python"
from space_gass_api import SpaceGassApiClient
import space_gass_api.models as models
client = SpaceGassApiClient.create_client("http://localhost:34560")
```
```bash title="curl"
# No client setup needed — just make HTTP requests directly.
curl http://localhost:34560/api/v1/service/info
```
1. **Open a Sample Project**
`Job.OpenSample` loads one of the built-in SPACE GASS samples as a
new unsaved job — perfect for getting going without your own `.sg`
file. List available samples with `GET /file/samples` (or
[browse them in Swagger](http://localhost:34560/swagger#/File/get_file_samples));
here we'll open `Portal Frame.SG`, which ships with every install.
When you have your own file, use `Job.Open` with `FilePath` instead.
See [File Handling](/guides/file-handling) for the full set of
open/save/close patterns.
```csharp title="C#"
await client.Job.OpenSample.PostAsync(
new OpenSampleRequest { FileName = "Portal Frame.SG" });
```
```python title="Python"
await client.job.open_sample.post(
models.OpenSampleRequest(file_name="Portal Frame.SG"))
```
```bash title="curl"
curl -X POST http://localhost:34560/api/v1/job/open-sample \
-H "Content-Type: application/json" \
-d '{"fileName": "Portal Frame.SG"}'
```
1. **Get All Nodes**
```csharp title="C#"
var nodes = await client.Job.Structure.Nodes.GetAsync();
foreach (var node in nodes)
{
Console.WriteLine($"Node {node.Id}: ({node.X}, {node.Y}, {node.Z})");
}
```
```python title="Python"
nodes = await client.job.structure.nodes.get()
for node in nodes:
print(f"Node {node.id}: ({node.x}, {node.y}, {node.z})")
```
```bash title="curl"
curl http://localhost:34560/api/v1/job/structure/nodes
```
1. **Close the Project**
```csharp title="C#"
await client.Job.Close.PostAsync();
```
```python title="Python"
await client.job.close.post()
```
```bash title="curl"
curl -X POST http://localhost:34560/api/v1/job/close
```
## Putting It All Together
Drop this complete program into the project you created in Step 3 and
run it. It opens the `Portal Frame.SG` sample, lists every node, and
closes cleanly.
```csharp title="C# (Program.cs)"
using SpaceGassApi;
using SpaceGassApi.Models;
var client = SpaceGassApiClient.CreateClient("http://localhost:34560");
try
{
await client.Job.OpenSample.PostAsync(
new OpenSampleRequest { FileName = "Portal Frame.SG" });
var nodes = await client.Job.Structure.Nodes.GetAsync();
foreach (var node in nodes!)
{
Console.WriteLine($"Node {node.Id}: ({node.X}, {node.Y}, {node.Z})");
}
}
finally
{
await client.Job.Close.PostAsync();
}
```
```python title="Python (main.py)"
import asyncio
from space_gass_api import SpaceGassApiClient
import space_gass_api.models as models
async def main():
client = SpaceGassApiClient.create_client("http://localhost:34560")
try:
await client.job.open_sample.post(
models.OpenSampleRequest(file_name="Portal Frame.SG"))
nodes = await client.job.structure.nodes.get()
for node in nodes:
print(f"Node {node.id}: ({node.x}, {node.y}, {node.z})")
finally:
await client.job.close.post()
if __name__ == "__main__":
asyncio.run(main())
```
```bash title="curl"
# Open the sample, list nodes, close the job. Run from a terminal —
# bash on Linux/macOS or Git Bash / WSL on Windows.
curl -X POST http://localhost:34560/api/v1/job/open-sample \
-H "Content-Type: application/json" \
-d '{"fileName": "Portal Frame.SG"}'
curl http://localhost:34560/api/v1/job/structure/nodes
curl -X POST http://localhost:34560/api/v1/job/close
```
## Run this example locally
The same code is in the repo as
[`Example.QuickStart`](https://github.com/SpaceGass/space-gass-api/tree/main/sdks/csharp/examples/Example.QuickStart) (C#) and
[`quick_start`](https://github.com/SpaceGass/space-gass-api/tree/main/sdks/python/examples/quick_start) (Python).
Clone the repo and run it directly — no copy-paste:
```bash title="C#"
git clone https://github.com/SpaceGass/space-gass-api.git
cd space-gass-api/sdks/csharp/examples/Example.QuickStart
dotnet run
```
```bash title="Python"
git clone https://github.com/SpaceGass/space-gass-api.git
cd space-gass-api/sdks/python/examples/quick_start
pip install space-gass-api # once per environment
python quick_start.py
```
Make sure the SPACE GASS API service is running first (step 1 above).
## Next Steps
- [Simple Beam Model](/examples/simple-beam) — Full walkthrough
building a model, running analysis, and querying results
- [File Handling](/guides/file-handling) — Open, save, force-open, and
file status checks
- [API Reference](/api) — Browse all endpoints interactively
---
## Document: Overview
Start here — programmatic access to SPACE GASS. Machine-readable OpenAPI spec: https://api.spacegass.com/docs/api/1/schema.json
URL: /docs/overview
# Overview
The SPACE GASS API provides programmatic access to SPACE GASS structural
analysis data. Use it to open job files, read and edit structural
entities, run analyses, and query results.
The API is a **headless service** — it runs as a local server on your
machine without any program UI. There is no interaction with the
SPACE GASS desktop interface. All operations are performed through
HTTP requests to the local server.
## What You Can Do
- **Open and manage job files** — Create, open, save, and close
SPACE GASS `.sg` project files
- **Read, write, and edit structural data** — Nodes, members, sections,
materials, load cases, load categories, and load case groups. Create
or update many entities at once using bulk operations.
- **Run analyses** — Start linear, non-linear, buckling, and dynamic
frequency analyses, and monitor progress in real time
- **Query analysis results** — Retrieve node displacements, reactions,
member forces, buckling load factors, and more
## Prerequisites
To get access to the API, follow the steps in the
[Quick Start](/quick-start). You will need:
- [SPACE GASS](https://www.spacegass.com/) **14.5 or later** installed on your machine
- SPACE GASS has been opened at least once to initialize the required
data files
## Licensing
API access is included with Pro and above subscriptions, or available
as a yearly add-on for perpetual license holders. See
[Licensing](/licensing) for details or visit
[spacegass.com](https://www.spacegass.com/) for pricing.
## Authentication
No authentication is required. The API runs locally and is accessed
over plain HTTP. See [Authentication](/authentication) for
details.
## SDK Clients
Generated SDK clients are available for:
- **C#** — `SpaceGassApi` (NuGet package)
- **Python** — `space-gass-api` (pip installable)
Both are generated from the OpenAPI specification using
[Microsoft Kiota](https://learn.microsoft.com/en-us/openapi/kiota/overview).
The machine-readable spec is published at
[`api/1/schema.json`](https://api.spacegass.com/docs/api/1/schema.json)
if you want to generate a client for another language or feed it to an LLM.
## Using with AI coding agents
Building scripts with an AI assistant (Claude Code, Cursor, Copilot, ChatGPT)?
Point it at these single, clean, machine-readable entry points instead of
letting it crawl the source repo:
- **Full docs bundle for LLMs:** [`llms-full.txt`](https://api.spacegass.com/docs/llms-full.txt)
- **OpenAPI spec (JSON):** [`api/1/schema.json`](https://api.spacegass.com/docs/api/1/schema.json)
- **Repo as an MCP server** (search docs, examples, and code with zero setup):
[`gitmcp.io/SpaceGass/space-gass-api`](https://gitmcp.io/SpaceGass/space-gass-api)
Or paste this primer straight into your AI assistant to point it at everything
in one step:
```text
You have access to the SPACE GASS structural-analysis API — a LOCAL, no-auth
service at http://localhost:34560 (base path /api/v1). Load these first:
- Full docs bundle: https://api.spacegass.com/docs/llms-full.txt
- OpenAPI spec: https://api.spacegass.com/docs/api/1/schema.json
- Repo via GitMCP: https://gitmcp.io/SpaceGass/space-gass-api
SDKs (Kiota, fluent builder chains, async):
- Python: pip install space-gass-api -> from space_gass_api import SpaceGassApiClient
- C#: dotnet add package SpaceGassApi
Both expose SpaceGassApiClient.CreateClient() (no auth, base URL auto-set).
Use raw.githubusercontent.com for repo files, never /blob/ URLs.
```
## Next Steps
- [Quick Start](/quick-start) — Get up and running
in 5 minutes
- [Simple Beam Model](/examples/simple-beam) — Full walkthrough
building a model, running analysis, and querying results
- [API Reference](/api) — Browse all endpoints interactively
---
## Document: Licensing
SPACE GASS license requirements for API access
URL: /docs/licensing
# Licensing
The SPACE GASS API is licensed as an **add-on** to your existing
SPACE GASS license. API access is bundled into **Pro and above**
subscriptions, or available as a **yearly add-on** for perpetual
license holders.
## How License Seats Are Consumed
### API License
When the API service is started, an **API license seat** is consumed
and held for the duration of that service process. The seat is
released when the service is stopped.
### SPACE GASS License
When you open a job through the API, a **standard SPACE GASS license
seat** is consumed — the same as opening a job in the desktop
application. The seat is released when you close the job.
### Module Licenses
While working with an open job, additional module licenses (e.g.
plates, master-slave constraints) are consumed as needed. They are
released when you close the job.
## API Modes: ReadWrite and ReadOnly
The API can run in two modes that control how license seats are consumed:
### ReadWrite (default)
In **readwrite** mode the API holds a full SPACE GASS license seat and
can open jobs, create/modify entities, run analyses, and save files —
every endpoint is available.
### ReadOnly
In **readonly** mode the API releases the SPACE GASS license seat,
freeing it for use by the desktop application or another API instance.
While in readonly mode, only read operations are available — you can
query structure, loads, and results, but you cannot create, modify,
or delete entities, run analyses, or save files.
### Switching Modes
Use the `POST /service/access-mode` endpoint to transition between modes:
```csharp title="C#"
// Switch to readonly — releases the SPACE GASS license seat
await client.Service.AccessMode.PostAsync(new AccessModeUpdate { AccessMode = AccessMode.ReadOnly });
// Switch back to readwrite — re-acquires the license seat
await client.Service.AccessMode.PostAsync(new AccessModeUpdate { AccessMode = AccessMode.ReadWrite });
```
```python title="Python"
# Switch to readonly — releases the SPACE GASS license seat
await client.service.access_mode.post(models.AccessModeUpdate(access_mode=models.AccessMode.ReadOnly))
# Switch back to readwrite — re-acquires the license seat
await client.service.access_mode.post(models.AccessModeUpdate(access_mode=models.AccessMode.ReadWrite))
```
```bash title="curl"
# Switch to readonly
curl -X POST http://localhost:34560/api/v1/service/access-mode \
-H "Content-Type: application/json" \
-d '{"accessMode": "ReadOnly"}'
# Switch back to readwrite
curl -X POST http://localhost:34560/api/v1/service/access-mode \
-H "Content-Type: application/json" \
-d '{"accessMode": "ReadWrite"}'
```
If a job is open when you switch to readonly, the transition is
**queued** (HTTP 202) and commits when the job closes. Check the
current mode and any pending transition via `GET /service/access-mode`
(the `accessMode` and `pendingAccessMode` fields).
If the license seat cannot be re-acquired when switching back to
readwrite (e.g. all seats are in use), the API returns `409 Conflict`
and stays in readonly.
## More Information
For full details on SPACE GASS licensing and pricing, visit
[spacegass.com](https://www.spacegass.com/).
---
## Document: Concepts
The mental model for working with the SPACE GASS API — the active job, the entity model, units, and common pitfalls
URL: /docs/concepts
# Concepts
This page is the conceptual map of the API. It does not show code — the
[Quick Start](/quick-start) and the
[Examples](/examples/simple-beam) cover that. Read this once
and the calls in those examples will make sense.
## The Active Job
The API is **stateful** with respect to a single active job. At any
moment the service is in one of two states:
- **No job open** — most endpoints under `/job/...` will return
`409 Conflict` until you open one.
- **One job open** — every `/job/...` call acts on that job. There is
no way to open two jobs simultaneously.
You move between states with three endpoints:
| Endpoint | When to use |
|---|---|
| `POST /job/new` | Start fresh, in-memory only — no file on disk yet. |
| `POST /job/open` | Load an existing `.sg` file from disk into the active job. |
| `POST /job/open-sample` | Load one of the built-in sample projects (`Portal Frame.SG`, etc.) — useful for getting started without a file of your own. List them at `GET /file/samples`. |
| `POST /job/save` | Persist the current job (use `Save As` semantics by passing a `filePath`). |
| `POST /job/close` | Close the active job and return to the no-job-open state. |
The recommended pattern in your code is **`open` → work → `close`** in
a `try / finally` so the job is always cleaned up, even if a step in
the middle throws. The [Quick Start](/quick-start) and
the [Simple Beam tutorial](/examples/simple-beam) both follow
this pattern.
### Why this matters
If you call `GET /job/structure/nodes` (or any other entity endpoint)
without an open job, you'll see a `409 Conflict` with a
`ErrorResponse` body explaining the active-job requirement. This is
the most common error a new user hits. The fix is always: open a job
first.
## The Entity Model
A SPACE GASS job is built from a small number of entity types. Most of
the API surface is CRUD over these:
| Entity | Path prefix | What it represents |
|---|---|---|
| **Node** | `/job/structure/nodes` | A point in 3D space (x, y, z). Identified by an `Id`. |
| **Member** | `/job/structure/members` | A 1D line element between two nodes. References a section + material. |
| **Plate** | `/job/structure/plates` | A 2D shell element on three or four nodes. References a section + material. |
| **Section** | `/job/structure/sections` | A cross-section (user-defined or pulled from a library). |
| **Material** | `/job/structure/materials` | A material (user-defined or pulled from a library). |
| **Node restraint** | `/job/structure/node-restraints` | A support condition (fixed, pinned, spring, etc.) attached to a node. Top-level entity, not a sub-resource of nodes. |
| **Node constraint** | `/job/structure/node-constraints` | A master/slave kinematic relationship between nodes. |
| **Member offset** | `/job/structure/member-offsets` | Rigid end zones at each end of a member. Top-level entity. |
| **Load case** | `/job/loads/load-cases` | A grouping of loads (primary, e.g. dead, live, wind). |
| **Combination case** | `/job/loads/combination-load-cases` | A weighted combination of primary cases (e.g. ULS, SLS). |
| **Loads** | `/job/loads/...` | Node loads, member distributed loads, member concentrated loads, plate pressure loads, prestress loads, thermal loads, self-weight loads, etc. — each its own collection. |
### Collection vs single-item endpoints
Every entity follows a uniform shape:
- `GET /…/` returns the whole collection (often supports
filtering and pagination — see
[Filtering & Querying](/guides/filtering-and-querying)).
- `POST /…/` creates one entity from a `Create` body.
The response includes the `Id` SPACE GASS allocated.
- `POST /…//bulk` creates many at once with a list body — see
[Bulk Operations](/guides/bulk-operations).
- `GET /…//{id}` reads one entity.
- `PATCH /…//{id}` partially updates one entity from an
`Update` body — only fields included in the request change.
- `DELETE /…//{id}` removes one entity.
The SDK's typed methods mirror this 1:1.
### Ids are allocated by SPACE GASS
For most entities you don't pick the `Id` — POST returns the saved
object with the `Id` populated, and you hold onto that for any later
calls that reference it. Two entity families are an exception, where
you supply a meaningful integer key directly:
- **Load cases** and **combination cases** — you provide the `Id` in
the create body so they have stable user-visible numbers. The
numeric Id IS the user-meaningful name.
- **Self-weight loads** — keyed by the load case Id (one per case).
## Units
The API is unit-agnostic. Numeric values are interpreted according to
whatever unit set the active job is configured with. Read or change the
active set via `GET /job/units` and `PATCH /job/units`. Most model
field descriptions in the [API Reference](/api) tell you which
unit family applies (Length, Force, Section Properties, etc.).
If you create a model entirely from the API (no opened `.sg` file),
units default to SPACE GASS's standard SI set. Check first if you're
unsure.
## Analysis Runs
Running an analysis is asynchronous. The pattern is:
1. **Configure** with `PATCH /job/settings/...` (e.g. solver, optimisation).
2. **Start** with `POST /job/analysis/static/run-linear` (or the
nonlinear / buckling / dynamic counterpart). This returns a `runId`
and an initial status of `Running` — it does **not** wait.
3. **Poll** with `GET /job/analysis/runs/{runId}` until `status` is
`Completed`, `Failed`, or `Cancelled`. The response includes a
`progress` block (current step, percentage, load-case status) you
can render.
4. **Query** results via the static / dynamic / buckling endpoints
under `/job/query/analysis/...`. Each result endpoint returns one
row per (case, entity) combination.
If a case asked for hasn't been analysed (or any of its constituent
primaries, for combinations), it appears in
`Warnings.LoadCasesNotAnalyzed` in the result envelope — check this before
reading `Results` so a missing run doesn't silently look like
zero rows.
See [Running Analysis](/guides/running-analysis) for the full pattern
with both languages, including `WaitForCompletion` helpers.
## Common Pitfalls
| Symptom | Cause | Fix |
|---|---|---|
| `409 Conflict` on every endpoint | No active job. | Open one with `POST /job/new`, `POST /job/open`, or `POST /job/open-sample`. |
| `404 Not Found` on a node / member you just created | Wrong Id, or the entity was created in a previous job that has since been closed. | The Id is returned on POST — capture and reuse it within the same active job. |
| Result `Warnings.LoadCasesNotAnalyzed` is non-empty | The combination's primary cases (or the case itself) haven't been analysed. | Run the relevant analysis before querying, or filter to only analysed cases. |
| `409 Conflict` on creating a restraint / constraint / offset | One already exists for that node / member. They are entity-style: at most one per parent. | `DELETE` the existing one first, or `PATCH` to update it. |
| Numbers come back wrong by a factor of 1000 | Unit mismatch — your code assumed kN, model is in N (or vice versa). | Check / set `GET /job/units` before sending or interpreting numeric data. |
## Next Steps
- [Quick Start](/quick-start) — open the `Portal Frame.SG`
sample and read its nodes in five minutes.
- [Using the SDK](/using-the-sdk) — REST verbs and
async patterns translated for engineers.
- [Simple Beam Model](/examples/simple-beam) — the full
build-to-result walkthrough.
---
## Document: Authentication
Authentication for the SpaceGass API
URL: /docs/authentication
# Authentication
The SPACE GASS API currently requires **no authentication**. The API
runs as a local service on your machine and is accessed over plain
HTTP — no API keys, tokens, or credentials are needed.
## Default Connection
The SDK clients connect to the local API with no setup:
```csharp title="C#"
using SpaceGassApi;
var client = SpaceGassApiClient.CreateClient("http://localhost:34560");
// Connects to http://localhost:34560/api/v1
```
```python title="Python"
from space_gass_api import SpaceGassApiClient
client = SpaceGassApiClient.create_client("http://localhost:34560")
# Connects to http://localhost:34560/api/v1
```
```bash title="curl"
curl http://localhost:34560/api/v1/service/info
```
## Custom Base URL
If the API is running on a different port, pass the URL explicitly:
```csharp title="C#"
var client = SpaceGassApiClient.CreateClient("http://localhost:8080");
```
```python title="Python"
client = SpaceGassApiClient.create_client("http://localhost:8080")
```
## Future Authentication
API key authentication may be introduced in a future release.
---
## Document: Service Automation
Start, wait for, use, and stop the SPACE GASS API service from your own code
URL: /docs/guides/service-automation
# Service Automation
The SPACE GASS API runs as a local HTTP service launched from the
SpaceGassApi executable installed with SPACE GASS. For interactive use
you can double-click the shortcut and leave it running, but for
scripts and batch jobs you usually want your code to manage the
service itself — start it on demand, wait until it is ready, run your
work, and shut it down cleanly when done.
The SDK does not ship a service-lifecycle helper. The pattern is short
enough to keep in your own project; the boilerplate below is a
copy-paste starting point for both C# and Python.
## The Lifecycle
A typical script does four things:
1. **Probe** — try `Service.Info` to see if the service is already
running. If yes, reuse it (don't kill someone else's instance).
2. **Start** — if not running, launch `SpaceGassApi.exe` as a child
process.
3. **Wait** — poll `Service.Info` until it responds, or fail with a
timeout if the service never comes up.
4. **Stop** — when your script is finished, terminate the child
process if (and only if) you started it. Skip cleanup if the
service was already running before your script ran.
Wrap steps 2–4 in a `try / finally` (C#) or context manager (Python)
so the service still shuts down if your code throws.
## Boilerplate
The example below probes for an existing service, starts one if
needed, waits up to 30 seconds for it to become ready, fetches
`Service.Info` to confirm, then shuts the service down.
```csharp title="C#"
using System.Diagnostics;
using SpaceGassApi;
const string ServiceExePath = @"C:\Program Files\SPACE GASS 14.5\SpaceGassApi.exe";
var client = SpaceGassApiClient.CreateClient("http://localhost:34560");
Process? serviceProcess = null;
try
{
// 1. Probe — reuse an already-running service if there is one
if (!await IsServiceReadyAsync(client))
{
// 2. Start
Console.WriteLine("Starting the SPACE GASS API service...");
serviceProcess = Process.Start(new ProcessStartInfo
{
FileName = ServiceExePath,
UseShellExecute = false,
CreateNoWindow = true,
});
// 3. Wait — poll until ready or fail with a timeout
await WaitForServiceReadyAsync(client, TimeSpan.FromSeconds(30));
Console.WriteLine("Service is ready.");
}
else
{
Console.WriteLine("Service was already running — reusing it.");
}
// Do your work here
var info = await client.Service.Info.GetAsync();
Console.WriteLine($"Connected to SPACE GASS {info?.SpaceGassVersion} at {info?.ApiPath}");
}
finally
{
// 4. Stop — only if we started it
if (serviceProcess is not null && !serviceProcess.HasExited)
{
Console.WriteLine("Stopping the service...");
serviceProcess.Kill(entireProcessTree: true);
serviceProcess.WaitForExit();
}
}
static async Task IsServiceReadyAsync(SpaceGassApiClient c)
{
try
{
await c.Service.Info.GetAsync();
return true;
}
catch
{
return false;
}
}
static async Task WaitForServiceReadyAsync(SpaceGassApiClient c, TimeSpan timeout)
{
var deadline = DateTime.UtcNow + timeout;
while (DateTime.UtcNow < deadline)
{
if (await IsServiceReadyAsync(c)) return;
await Task.Delay(500);
}
throw new TimeoutException("SPACE GASS API service did not become ready in time.");
}
```
```python title="Python"
import asyncio
import subprocess
import time
from space_gass_api import SpaceGassApiClient
SERVICE_EXE = r"C:\Program Files\SPACE GASS 14.5\SpaceGassApi.exe"
async def is_service_ready(client) -> bool:
try:
await client.service.info.get()
return True
except Exception:
return False
async def wait_for_service_ready(client, timeout: float = 30.0) -> None:
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if await is_service_ready(client):
return
await asyncio.sleep(0.5)
raise TimeoutError("SPACE GASS API service did not become ready in time.")
async def main() -> None:
client = SpaceGassApiClient.create_client("http://localhost:34560")
process: subprocess.Popen | None = None
try:
# 1. Probe — reuse an already-running service if there is one
if not await is_service_ready(client):
# 2. Start
print("Starting the SPACE GASS API service...")
process = subprocess.Popen(
[SERVICE_EXE],
creationflags=subprocess.CREATE_NO_WINDOW,
)
# 3. Wait
await wait_for_service_ready(client)
print("Service is ready.")
else:
print("Service was already running — reusing it.")
# Do your work here
info = await client.service.info.get()
print(f"Connected to SPACE GASS {info.space_gass_version} at {info.api_path}")
finally:
# 4. Stop — only if we started it
if process is not None and process.poll() is None:
print("Stopping the service...")
process.terminate()
try:
process.wait(timeout=5)
except subprocess.TimeoutExpired:
process.kill()
if __name__ == "__main__":
asyncio.run(main())
```
## Custom Ports
To run the service on a non-default port — useful if `34560` is in use,
or if you want to run multiple instances side by side — pass `--port`
to the executable and the matching base URL to `CreateClient`:
```csharp title="C#"
const int Port = 35000;
var client = SpaceGassApiClient.CreateClient($"http://localhost:{Port}/api/v1");
serviceProcess = Process.Start(new ProcessStartInfo
{
FileName = ServiceExePath,
Arguments = $"--port={Port}",
UseShellExecute = false,
CreateNoWindow = true,
});
```
```python title="Python"
PORT = 35000
client = SpaceGassApiClient.create_client(f"http://localhost:{PORT}/api/v1")
process = subprocess.Popen(
[SERVICE_EXE, f"--port={PORT}"],
creationflags=subprocess.CREATE_NO_WINDOW,
)
```
## Handling Ctrl+C
The `try / finally` (C#) and context manager (Python) above handle
exceptions correctly, but a hard interrupt (Ctrl+C) skips them by
default. Wire a signal handler so the service still shuts down on
abort:
```csharp title="C#"
Console.CancelKeyPress += (_, e) =>
{
if (serviceProcess is not null && !serviceProcess.HasExited)
serviceProcess.Kill(entireProcessTree: true);
};
```
```python title="Python"
import signal
def _shutdown(*_):
if process is not None and process.poll() is None:
process.terminate()
signal.signal(signal.SIGINT, _shutdown)
signal.signal(signal.SIGTERM, _shutdown)
```
## Tips
- **Locate the executable** — the install path varies by SPACE GASS
version. Read it from configuration or an environment variable
rather than hard-coding `C:\Program Files\SPACE GASS 14.5\` so the
same script keeps working after an upgrade.
- **One service at a time on the default port** — if `34560` is
already bound (perhaps by an interactive instance the user
launched), starting another on the same port will fail. Either pick
a different port or accept the existing one (the boilerplate above
does the latter).
- **Don't kill a service you didn't start** — the `finally` block
guards on `serviceProcess != null`, so a script that found an
already-running service leaves it running. Useful when developing
interactively in the same session as a long-lived service.
- **Surface service stdout** — for debugging, redirect the child
process's `StandardOutput` / `StandardError` to a log file. The
boilerplate uses `CreateNoWindow = true` so the service runs
invisibly; flip that off (or omit it) if you want the console to
show service messages.
## Run this example locally
The complete program with error handling and Ctrl+C wiring is in the
repo as
[`Example.ServiceAutomation`](https://github.com/SpaceGass/space-gass-api/tree/main/sdks/csharp/examples/Example.ServiceAutomation) (C#) and
[`service_automation`](https://github.com/SpaceGass/space-gass-api/tree/main/sdks/python/examples/service_automation) (Python).
Clone the repo and run it directly — no copy-paste:
```bash title="C#"
git clone https://github.com/SpaceGass/space-gass-api.git
cd space-gass-api/sdks/csharp/examples/Example.ServiceAutomation
dotnet run
```
```bash title="Python"
git clone https://github.com/SpaceGass/space-gass-api.git
cd space-gass-api/sdks/python/examples/service_automation
pip install space-gass-api # once per environment
python service_automation.py
```
Edit the `ServiceExePath` / `SERVICE_EXE` constant first to match the
SPACE GASS install path on your machine.
---
## Document: Running Analysis
How to run analyses, poll for progress, and handle async workflows
URL: /docs/guides/running-analysis
# Running Analysis
Analysis in the SPACE GASS API is **asynchronous** — when you start
an analysis, the API returns immediately with a run ID. You then poll
for progress until the run completes. This guide covers the common
patterns from simple scripting to advanced multi-run monitoring.
## How It Works
1. **Start** an analysis — returns an `AnalysisRun` with a `runId` and initial status
2. **Poll** the run status using the `runId` at regular intervals
3. **Check** for a terminal status: `Completed`, `Failed`, or `Cancelled`
4. **Query** results once completed
## Simple Script — Wait for Completion
Most scripts just need to run an analysis and wait. Here is a simple
helper that polls until the analysis finishes, then returns the
final status:
```csharp title="C#"
using SpaceGassApi;
using SpaceGassApi.Models;
var client = SpaceGassApiClient.CreateClient("http://localhost:34560");
// Open a project and run linear static analysis
await client.Job.Open.PostAsync(
new OpenJobRequest { FilePath = @"C:\Projects\MyStructure.sg" });
var run = await client.Job.Analysis.Static.RunLinear.PostAsync(
new StaticSettingsUpdate());
// Poll until complete
var result = await WaitForCompletion(client, run.RunId.Value);
Console.WriteLine($"Analysis {result.Status} in {result.ElapsedTime}");
if (result.Status == AnalysisRunStatus.Completed)
{
// Query results...
var reactions = await client.Job.Query.Analysis.Static.NodeReactions.GetAsync();
Console.WriteLine($"Got {reactions.Results.Count} reactions");
}
await client.Job.Close.PostAsync();
// --- Helper: poll until terminal state ---
static async Task WaitForCompletion(
SpaceGassApiClient client, Guid runId, int pollMs = 500)
{
while (true)
{
await Task.Delay(pollMs);
var status = await client.Job.Analysis.Runs[runId].GetAsync();
if (status.Status is AnalysisRunStatus.Completed
or AnalysisRunStatus.Failed
or AnalysisRunStatus.Cancelled)
{
return status;
}
}
}
```
```python title="Python"
import asyncio
from space_gass_api import SpaceGassApiClient
import space_gass_api.models as models
client = SpaceGassApiClient.create_client("http://localhost:34560")
# Open a project and run linear static analysis
await client.job.open.post(
models.OpenJobRequest(file_path="C:\\Projects\\MyStructure.sg"))
run = await client.job.analysis.static.run_linear.post(
models.StaticSettingsUpdate())
# Poll until complete
async def wait_for_completion(client, run_id, poll_interval=0.5):
while True:
await asyncio.sleep(poll_interval)
status = await client.job.analysis.runs.by_run_id(str(run_id)).get()
if status.status in (
models.AnalysisRunStatus.Completed,
models.AnalysisRunStatus.Failed,
models.AnalysisRunStatus.Cancelled,
):
return status
result = await wait_for_completion(client, run.run_id)
print(f"Analysis {result.status} in {result.elapsed_time}")
if result.status == models.AnalysisRunStatus.Completed:
# Query results...
reactions = await client.job.query.analysis.static.node_reactions.get()
print(f"Got {len(reactions.results)} reactions")
await client.job.close.post()
```
The `WaitForCompletion` helper turns the async polling into a simple
blocking call — your script just waits until the analysis is done
before moving on. This is the pattern you will use most often in
scripts.
## Pre-Check with Analysis Info
Before running an analysis, you can call the **Info** endpoint to see
which load cases already have results — and which still need to be
analysed. This avoids re-running cases that are already complete,
which saves time on large models.
`Analysis.Static.Info` (and the equivalent `Buckling.Info` and
`DynamicFrequency.Info`) returns:
| Field | Description |
|---|---|
| `HasResults` | `true` if any case has stored results for this analysis type |
| `LoadCases` | Per-case breakdown — each entry has a `LoadCase` Id and `HasResults` flag |
| `NotAnalyzed` | SG list-format string of case Ids that have **not** been analysed — ready to pass straight into a run request |
### Check which cases have been analysed
```csharp title="C#"
var info = await client.Job.Analysis.Static.Info.GetAsync();
Console.WriteLine($"Has any results: {info.HasResults}");
foreach (var c in info.LoadCases!)
{
Console.WriteLine($" LC {c.LoadCase}: {(c.HasResults == true ? "analysed" : "not analysed")}");
}
if (info.NotAnalyzed != null)
Console.WriteLine($"Not analysed: {info.NotAnalyzed}");
```
```python title="Python"
info = await client.job.analysis.static.info.get()
print(f"Has any results: {info.has_results}")
for c in info.load_cases:
status = "analysed" if c.has_results else "not analysed"
print(f" LC {c.load_case}: {status}")
if info.not_analyzed:
print(f"Not analysed: {info.not_analyzed}")
```
```bash title="curl"
curl http://localhost:34560/api/v1/job/analysis/static/info
```
### Filter to a specific range
Pass a `loadCases` filter to check a subset. The `NotAnalyzed` field
returns only the intersection of your filter with the not-analysed
set — you can feed it directly into the run request.
```csharp title="C#"
// Check cases 1 through 10
var info = await client.Job.Analysis.Static.Info.GetAsync(
config => config.QueryParameters.LoadCases = "1-10");
if (info.NotAnalyzed != null)
{
Console.WriteLine($"Cases 1-10 not yet analysed: {info.NotAnalyzed}");
// Run only the missing cases
var run = await client.Job.Analysis.Static.RunLinear.PostAsync(
new StaticSettingsUpdate { LoadCases = info.NotAnalyzed });
var result = await WaitForCompletion(client, run.RunId.Value);
Console.WriteLine($"Analysis {result.Status}");
}
else
{
Console.WriteLine("Cases 1-10 are all analysed — nothing to run.");
}
```
```python title="Python"
# Check cases 1 through 10
info = await client.job.analysis.static.info.get(load_cases="1-10")
if info.not_analyzed:
print(f"Cases 1-10 not yet analysed: {info.not_analyzed}")
# Run only the missing cases
run = await client.job.analysis.static.run_linear.post(
models.StaticSettingsUpdate(load_cases=info.not_analyzed))
result = await wait_for_completion(client, run.run_id)
print(f"Analysis {result.status}")
else:
print("Cases 1-10 are all analysed — nothing to run.")
```
```bash title="curl"
# Check cases 1-10
curl "http://localhost:34560/api/v1/job/analysis/static/info?loadCases=1-10"
# If notAnalyzed is "4,7-10", run only those
curl -X POST http://localhost:34560/api/v1/job/analysis/static/run-linear \
-H "Content-Type: application/json" \
-d '{"loadCases": "4,7-10"}'
```
This pattern is especially useful in automation workflows where
a model is re-analysed incrementally — new load cases are added and
only the missing ones need to be run, leaving existing results intact.
## Running Multiple Analyses in Sequence
The API processes analysis runs in a queue. You can start multiple
analyses and they will execute in order. Poll the last run ID to
know when all analyses are complete:
```csharp title="C#"
// Queue three analysis types
var staticRun = await client.Job.Analysis.Static.RunLinear.PostAsync(
new StaticSettingsUpdate());
Console.WriteLine($"Queued linear static: {staticRun.RunId}");
var bucklingRun = await client.Job.Analysis.Buckling.Run.PostAsync(
new BucklingSettingsUpdate());
Console.WriteLine($"Queued buckling: {bucklingRun.RunId}");
var dynamicRun = await client.Job.Analysis.DynamicFrequency.Run.PostAsync(
new DynamicFrequencySettingsUpdate());
Console.WriteLine($"Queued dynamic frequency: {dynamicRun.RunId}");
// Wait for each in order
var staticResult = await WaitForCompletion(client, staticRun.RunId.Value);
Console.WriteLine($"Static: {staticResult.Status} ({staticResult.ElapsedTime})");
var bucklingResult = await WaitForCompletion(client, bucklingRun.RunId.Value);
Console.WriteLine($"Buckling: {bucklingResult.Status} ({bucklingResult.ElapsedTime})");
var dynamicResult = await WaitForCompletion(client, dynamicRun.RunId.Value);
Console.WriteLine($"Dynamic: {dynamicResult.Status} ({dynamicResult.ElapsedTime})");
```
```python title="Python"
# Queue three analysis types
static_run = await client.job.analysis.static.run_linear.post(
StaticSettingsUpdate())
print(f"Queued linear static: {static_run.run_id}")
buckling_run = await client.job.analysis.buckling.run.post(
BucklingSettingsUpdate())
print(f"Queued buckling: {buckling_run.run_id}")
dynamic_run = await client.job.analysis.dynamic_frequency.run.post(
DynamicFrequencySettingsUpdate())
print(f"Queued dynamic frequency: {dynamic_run.run_id}")
# Wait for each in order
static_result = await wait_for_completion(client, static_run.run_id)
print(f"Static: {static_result.status} ({static_result.elapsed_time})")
buckling_result = await wait_for_completion(client, buckling_run.run_id)
print(f"Buckling: {buckling_result.status} ({buckling_result.elapsed_time})")
dynamic_result = await wait_for_completion(client, dynamic_run.run_id)
print(f"Dynamic: {dynamic_result.status} ({dynamic_result.elapsed_time})")
```
## Monitoring Progress
For longer analyses you may want to display progress as it runs.
The `AnalysisRun.Progress` object provides step labels, iteration
percentages, and load case status:
```csharp title="C#"
var lastStep = -1;
while (true)
{
await Task.Delay(500);
var status = await client.Job.Analysis.Runs[runId].GetAsync();
if (status.Progress != null)
{
var p = status.Progress;
var stepInfo = $"Step {p.CurrentStep}/{p.TotalSteps}";
// Print step label when it changes
if (p.CurrentStep != lastStep && p.StepLabels != null)
{
var idx = p.CurrentStep ?? 0;
if (idx < p.StepLabels.Count && !string.IsNullOrEmpty(p.StepLabels[idx]))
Console.WriteLine($" [{stepInfo}] {p.StepLabels[idx]}");
lastStep = p.CurrentStep ?? -1;
}
// Overwrite current line with live progress
Console.Write($"\r {stepInfo} | {p.IterationPercentage}%");
if (p.LoadCaseStatus != null)
Console.Write($" | {p.LoadCaseStatus}");
Console.Write("".PadRight(20));
}
if (status.Status is AnalysisRunStatus.Completed
or AnalysisRunStatus.Failed
or AnalysisRunStatus.Cancelled)
{
Console.WriteLine();
Console.WriteLine($"Analysis {status.Status} in {status.ElapsedTime}");
if (status.Warnings?.Count > 0)
Console.WriteLine($" Warnings: {status.Warnings.Count}");
if (status.ErrorMessage != null)
Console.WriteLine($" Error: {status.ErrorMessage}");
break;
}
}
```
```python title="Python"
last_step = -1
while True:
await asyncio.sleep(0.5)
status = await client.job.analysis.runs.by_run_id(str(run_id)).get()
if status.progress is not None:
p = status.progress
step_info = f"Step {p.current_step}/{p.total_steps}"
# Print step label when it changes
if p.current_step != last_step and p.step_labels:
idx = p.current_step or 0
if idx < len(p.step_labels) and p.step_labels[idx]:
print(f" [{step_info}] {p.step_labels[idx]}")
last_step = p.current_step if p.current_step is not None else -1
# Overwrite current line with live progress
line = f"\r {step_info} | {p.iteration_percentage}%"
if p.load_case_status:
line += f" | {p.load_case_status}"
print(f"{line:<60}", end="", flush=True)
if status.status in (
AnalysisRunStatus.Completed,
AnalysisRunStatus.Failed,
AnalysisRunStatus.Cancelled,
):
print()
print(f"Analysis {status.status} in {status.elapsed_time}")
if status.warnings:
print(f" Warnings: {len(status.warnings)}")
if status.error_message:
print(f" Error: {status.error_message}")
break
```
## Cancelling a Run
You can cancel a running or queued analysis by calling `DELETE` on
the run:
```csharp title="C#"
await client.Job.Analysis.Runs[runId].DeleteAsync();
```
```python title="Python"
await client.job.analysis.runs.by_run_id(str(run_id)).delete()
```
```bash title="curl"
curl -X DELETE http://localhost:34560/api/v1/job/analysis/runs/{runId}
```
The run transitions to `Cancelling` and then `Cancelled` once the
solver has stopped.
## Analysis Run Lifecycle
| Status | Description |
|---|---|
| `Queued` | Run is waiting in the queue |
| `Running` | Analysis is actively executing |
| `Cancelling` | Cancellation requested, waiting for solver to stop |
| `Completed` | Analysis finished successfully — results are available |
| `Failed` | Analysis encountered an error — check `errorMessage` |
| `Cancelled` | Analysis was cancelled before completion |
## Advanced: Real-Time Monitoring Application
For a full example of an interactive monitoring application that
tracks multiple concurrent runs with live progress bars, elapsed
timers, and error/warning display, see the
[AnalysisMonitor WPF example](https://github.com/SpaceGass/space-gass-api/tree/main/sdks/csharp/examples/Example.AnalysisMonitor)
in the repository.
---
## Document: Filtering & Querying
How to filter, query, and paginate API results
URL: /docs/guides/filtering-and-querying
# Filtering & Querying
Most API endpoints support query parameters for filtering, and all
query result endpoints support pagination. This guide covers the
common patterns.
## Filtering Entities
Structure endpoints like nodes, members, and sections support query
parameters to narrow down results.
```csharp title="C#"
// Get nodes within a coordinate range
var filtered = await client.Job.Structure.Nodes.GetAsync(
config =>
{
config.QueryParameters.MinX = 0;
config.QueryParameters.MaxX = 100;
config.QueryParameters.Limit = 50;
});
```
```python title="Python"
filtered = await client.job.structure.nodes.get(
min_x=0, max_x=100, limit=50)
```
```bash title="curl"
curl "http://localhost:34560/api/v1/job/structure/nodes?MinX=0&MaxX=100&Limit=50"
```
## Getting a Single Entity
Access any entity by its Id using the indexer (C#) or `by_id`
(Python):
```csharp title="C#"
var node = await client.Job.Structure.Nodes[1].GetAsync();
Console.WriteLine($"Node {node.Id}: ({node.X}, {node.Y}, {node.Z})");
```
```python title="Python"
node = await client.job.structure.nodes.by_id(1).get()
print(f"Node {node.id}: ({node.x}, {node.y}, {node.z})")
```
```bash title="curl"
curl http://localhost:34560/api/v1/job/structure/nodes/1
```
## Querying Analysis Results
After running an analysis, query endpoints return results in a flat,
efficient format. All query endpoints support optional filters for
load case and entity key.
### Node Reactions
```csharp title="C#"
var result = await client.Job.Query.Analysis.Static.NodeReactions.GetAsync();
foreach (var r in result.Results)
{
Console.WriteLine($"Case {r.LoadCase}, Node {r.Node}: " +
$"Fx={r.Fx}, Fy={r.Fy}, Fz={r.Fz}");
}
```
```python title="Python"
result = await client.job.query.analysis.static.node_reactions.get()
for r in result.results:
print(f"Case {r.load_case}, Node {r.node}: "
f"Fx={r.fx}, Fy={r.fy}, Fz={r.fz}")
```
```bash title="curl"
curl http://localhost:34560/api/v1/job/query/analysis/static/node-reactions
```
### Filtering by Load Case and Node
Filter values use **SG list format** — a string of comma-separated Ids
and dash-separated ranges (e.g. `"1,3-7,10"`). Pass an empty string
or omit the parameter to return all results.
```csharp title="C#"
var result = await client.Job.Query.Analysis.Static.NodeDisplacements
.GetAsync(config =>
{
config.QueryParameters.LoadCases = "1,3";
config.QueryParameters.Nodes = "10-12";
});
```
```python title="Python"
result = await client.job.query.analysis.static.node_displacements.get(
load_cases="1,3", nodes="10-12")
```
```bash title="curl"
curl "http://localhost:34560/api/v1/job/query/analysis/static/node-displacements?loadCases=1,3&nodes=10-12"
```
### Member Intermediate Forces
Grouped results like member forces use a columnar layout — the parent
object contains the case and member keys, with force values as parallel
arrays indexed by station.
```csharp title="C#"
var result = await client.Job.Query.Analysis.Static.MemberIntermediateForces
.GetAsync(config =>
{
config.QueryParameters.Members = "1";
});
foreach (var m in result.Results)
{
Console.WriteLine($"Case {m.LoadCase}, Member {m.Member}:");
for (int i = 0; i < m.Station.Length; i++)
{
Console.WriteLine($" Station {m.Station[i]}: " +
$"Fx={m.Fx[i]}, Fy={m.Fy[i]}, Mz={m.Mz[i]}");
}
}
```
```python title="Python"
result = await client.job.query.analysis.static.member_intermediate_forces.get(
members="1")
for m in result.results:
print(f"Case {m.load_case}, Member {m.member}:")
for i in range(len(m.station)):
print(f" Station {m.station[i]}: "
f"Fx={m.fx[i]}, Fy={m.fy[i]}, Mz={m.mz[i]}")
```
```bash title="curl"
curl "http://localhost:34560/api/v1/job/query/analysis/static/member-intermediate-forces?members=1"
```
### Buckling Load Factors
```csharp title="C#"
var result = await client.Job.Query.Analysis.Buckling.LoadFactors.GetAsync();
foreach (var b in result.Results)
{
Console.WriteLine($"Case {b.LoadCase}, Mode {b.Mode}: " +
$"Factor={b.LoadFactor}, Tolerance={b.Tolerance}");
}
```
```python title="Python"
result = await client.job.query.analysis.buckling.load_factors.get()
for b in result.results:
print(f"Case {b.load_case}, Mode {b.mode}: "
f"Factor={b.load_factor}, Tolerance={b.tolerance}")
```
```bash title="curl"
curl http://localhost:34560/api/v1/job/query/analysis/buckling/load-factors
```
### Natural Frequencies
```csharp title="C#"
var result = await client.Job.Query.Analysis.Dynamic.NaturalFrequencies
.GetAsync();
foreach (var f in result.Results)
{
Console.WriteLine($"Case {f.LoadCase}, Mode {f.Mode}: " +
$"{f.NaturalFrequency} Hz (period {f.NaturalPeriod}s)");
}
```
```python title="Python"
result = await client.job.query.analysis.dynamic.natural_frequencies.get()
for f in result.results:
print(f"Case {f.load_case}, Mode {f.mode}: "
f"{f.natural_frequency} Hz (period {f.natural_period}s)")
```
```bash title="curl"
curl http://localhost:34560/api/v1/job/query/analysis/dynamic/natural-frequencies
```
### Steel Design Check Summary
```csharp title="C#"
var result = await client.Job.Query.Design.SteelMember.CheckSummary.GetAsync();
foreach (var s in result.Results)
{
Console.WriteLine($"Member {s.Member}: {s.Section} — " +
$"{s.Failure} (factor {s.LoadFactor}, case {s.CriticalCase})");
}
```
```python title="Python"
result = await client.job.query.design.steel_member.check_summary.get()
for s in result.results:
print(f"Member {s.member}: {s.section} — "
f"{s.failure} (factor {s.load_factor}, case {s.critical_case})")
```
```bash title="curl"
curl http://localhost:34560/api/v1/job/query/design/steel-member/check-summary
```
## Pagination
All query endpoints support `offset` and `limit` for pagination. By
default all results are returned. Use pagination for large result sets.
```csharp title="C#"
// Get the first 100 node displacements
var page1 = await client.Job.Query.Analysis.Static.NodeDisplacements
.GetAsync(config =>
{
config.QueryParameters.Offset = 0;
config.QueryParameters.Limit = 100;
});
// Get the next 100
var page2 = await client.Job.Query.Analysis.Static.NodeDisplacements
.GetAsync(config =>
{
config.QueryParameters.Offset = 100;
config.QueryParameters.Limit = 100;
});
```
```python title="Python"
page1 = await client.job.query.analysis.static.node_displacements.get(
offset=0, limit=100)
page2 = await client.job.query.analysis.static.node_displacements.get(
offset=100, limit=100)
```
```bash title="curl"
# Page 1
curl "http://localhost:34560/api/v1/job/query/analysis/static/node-displacements?offset=0&limit=100"
# Page 2
curl "http://localhost:34560/api/v1/job/query/analysis/static/node-displacements?offset=100&limit=100"
```
---
## Document: File Handling
Working with SPACE GASS job files through the API
URL: /docs/guides/file-handling
# File Handling
The API provides endpoints for managing SPACE GASS `.sg` project files
— opening, saving, closing, and checking file status before operations.
## Opening a File
```csharp title="C#"
using SpaceGassApi.Models;
await client.Job.Open.PostAsync(
new OpenJobRequest
{
FilePath = @"C:\Projects\MyStructure.sg"
});
```
```python title="Python"
from space_gass_api.models.open_job_request import OpenJobRequest
await client.job.open.post(
OpenJobRequest(file_path="C:\\Projects\\MyStructure.sg"))
```
```bash title="curl"
curl -X POST http://localhost:34560/api/v1/job/open \
-H "Content-Type: application/json" \
-d '{"filePath": "C:\\Projects\\MyStructure.sg"}'
```
## Checking File Status
Before opening a file, you can check its status to determine if it
is locked by another process or has unsaved changes:
```csharp title="C#"
var status = await client.File.Status.GetAsync(config =>
config.QueryParameters.FilePath = @"C:\Projects\MyStructure.sg");
// status contains CanOpenSafely, Description, RecommendedAction
```
```python title="Python"
from kiota_abstractions.base_request_configuration import RequestConfiguration
from space_gass_api.file.status.status_request_builder import StatusRequestBuilder
query_params = StatusRequestBuilder.StatusRequestBuilderGetQueryParameters(
file_path=r"C:\Projects\MyStructure.sg")
status = await client.file.status.get(
request_configuration=RequestConfiguration(query_parameters=query_params))
```
```bash title="curl"
curl "http://localhost:34560/api/v1/file/status?filePath=C:%5CProjects%5CMyStructure.sg"
```
## Saving
The save endpoint handles both saving to the current path and
saving to a new path. Pass a `filePath` to save as a new file,
or omit it to save in place.
```csharp title="C#"
// Save to current file
await client.Job.Save.PostAsync(new SaveJobRequest());
// Save to a new file path
await client.Job.Save.PostAsync(
new SaveJobRequest
{
FilePath = @"C:\Projects\MyStructure_Copy.sg"
});
```
```python title="Python"
from space_gass_api.models.save_job_request import SaveJobRequest
# Save to current file
await client.job.save.post(SaveJobRequest())
# Save to a new file path
await client.job.save.post(
SaveJobRequest(file_path="C:\\Projects\\MyStructure_Copy.sg"))
```
```bash title="curl"
# Save to current file
curl -X POST http://localhost:34560/api/v1/job/save \
-H "Content-Type: application/json"
# Save to a new file path
curl -X POST http://localhost:34560/api/v1/job/save \
-H "Content-Type: application/json" \
-d '{"filePath": "C:\\Projects\\MyStructure_Copy.sg"}'
```
## Creating a New Job
```csharp title="C#"
await client.Job.New.PostAsync();
```
```python title="Python"
await client.job.new.post()
```
```bash title="curl"
curl -X POST http://localhost:34560/api/v1/job/new
```
## Creating a New Job From a Template
Upload a `.sgbase` template file to seed a new job. The template data
is loaded but the new job has no file path — use `Job.Save` with a
`FilePath` (see [Saving](#saving)) to write it to disk.
The body is a `multipart/form-data` upload with one part named
`template`. Both SDKs expose a `NewFromTemplateRequest` you construct
from a file path.
```csharp title="C#"
using SpaceGassApi;
await client.Job.NewFromTemplate.PostAsync(
new NewFromTemplateRequest(@"C:\Templates\my-template.sgbase"));
```
```python title="Python"
from space_gass_api import NewFromTemplateRequest
await client.job.new_from_template.post(
NewFromTemplateRequest(r"C:\Templates\my-template.sgbase"))
```
```bash title="curl"
curl -X POST http://localhost:34560/api/v1/job/new-from-template \
-F "template=@C:/Templates/my-template.sgbase"
```
## Closing
```csharp title="C#"
await client.Job.Close.PostAsync();
```
```python title="Python"
await client.job.close.post()
```
```bash title="curl"
curl -X POST http://localhost:34560/api/v1/job/close
```
---
## Document: Error Handling
How the SpaceGass API reports and handles errors
URL: /docs/guides/error-handling
# Error Handling
Every error response from the API uses a standard `ErrorResponse`
envelope. All error responses include a JSON body with diagnostic
information.
## Error Response Format
```json
{
"title": "Bad Request",
"status": 400,
"detail": "Node with Id 1 already exists",
"errorCode": "DUPLICATE_ENTITY",
"errors": [
{ "field": "id", "message": "Node with Id 1 already exists", "code": "DUPLICATE_ENTITY" }
],
"source": "Application",
"timestamp": "2026-05-18T10:30:00+10:00"
}
```
| Field | Type | Description |
|-------|------|-------------|
| `title` | `string` | Short human-readable summary |
| `status` | `int` | HTTP status code |
| `detail` | `string` | Human-readable explanation specific to this occurrence |
| `errorCode` | `string` | Machine-readable error code for programmatic handling |
| `errors` | `ValidationError[]` | One entry per failed validation rule (field, message, code) |
| `source` | `string` | Where the error originated (e.g. `"Application"`, `"Infrastructure"`) |
| `timestamp` | `string` | ISO 8601 timestamp when the error occurred |
| `instance` | `string` | URI reference for the specific occurrence |
| `extensions` | `object` | Additional context-specific data |
## HTTP Status Codes
| Status | Meaning |
|--------|---------|
| `400` | Bad request — validation failure or invalid input |
| `404` | Not found — resource does not exist |
| `409` | Conflict — operation conflicts with current state (e.g., file locked) |
| `500` | Server error — unexpected internal failure |
## Handling Errors in SDK Clients
The SDK raises a typed `ErrorResponse` exception that carries the
fields from the JSON body — `Status`, `Title`, `Detail`, `ErrorCode`,
`Errors`, and more. Catch it directly so you can inspect the structured
error rather than relying on the generic exception message:
```csharp title="C#"
using SpaceGassApi.Models;
try
{
var node = await client.Job.Structure.Nodes[999].GetAsync();
}
catch (ErrorResponse err) when (err.Status == 404)
{
Console.WriteLine($"Node not found: {err.Detail}");
}
catch (ErrorResponse err)
{
Console.WriteLine($"API error {err.Status} ({err.Title}): {err.Detail}");
if (err.ErrorCode != null)
Console.WriteLine($" Code: {err.ErrorCode}");
foreach (var ve in err.Errors ?? [])
Console.WriteLine($" [{ve.Field}] {ve.Message}");
}
```
```python title="Python"
from space_gass_api.models.error_response import ErrorResponse
try:
node = await client.job.structure.nodes.by_id(999).get()
except ErrorResponse as err:
if err.status == 404:
print(f"Node not found: {err.detail}")
else:
print(f"API error {err.status} ({err.title}): {err.detail}")
if err.error_code:
print(f" Code: {err.error_code}")
for ve in err.errors or []:
print(f" [{ve.field}] {ve.message}")
```
```bash title="curl"
# curl shows HTTP status in verbose mode
curl -v http://localhost:34560/api/v1/job/structure/nodes/999
# Response (404):
# {"title":"Not Found","status":404,"detail":"Node 999 not found","errorCode":"NOT_FOUND",...}
```
`ErrorResponse` inherits from Kiota's base `ApiException` (C#) /
`APIError` (Python), so you can still catch the broader type if you
need to handle transport errors (network drops, timeouts) the same way.
---
## Document: Bulk Operations
How bulk endpoints work, how they differ from single-item calls, and when to use them
URL: /docs/guides/bulk-operations
# Bulk Operations
Most collection endpoints — `Job.Structure.Nodes`,
`Job.Structure.Members`, the load primitives, and so on — expose two
shapes:
- **Single**: `Nodes.PostAsync(NodeCreate body)` — one entity per call
- **Bulk**: `Nodes.Bulk.PostAsync(List body)` — many entities per call
## Differences
| | Single | Bulk |
| --- | --- | --- |
| **Round trips** | One per item | One per batch |
| **Validation** | Each item is validated as the call arrives | The full batch is validated up front, before any items are created |
| **Failure response** | Throws on the first failing item | Returns a `*BulkResult` with `Succeeded` and `Errors` lists |
| **Partial commit on validation failure** | Items submitted before the failure are already created | Nothing is created unless every item validates |
| **Partial success** | Not applicable | Opt-in via `?continueOnError=true` |
## Example: creating 100 nodes
Bulk endpoints accept a `List` and return an `XBulkResult`
with the created entities under `Succeeded` and any failures under
`Errors`. `BulkError.Index` is the position in the input list, so you
can match an error back to the body you sent.
```csharp title="C#"
var bodies = Enumerable.Range(0, 100)
.Select(i => new NodeCreate { X = i % 10, Y = 0, Z = i / 10 })
.ToList();
var result = await client.Job.Structure.Nodes.Bulk.PostAsync(bodies);
foreach (var node in result!.Succeeded!)
{
Console.WriteLine($" Node {node.Id}: ({node.X}, {node.Y}, {node.Z})");
}
foreach (var error in result.Errors!)
{
Console.WriteLine($" [{error.Index}] {error.Error}");
}
```
```python title="Python"
bodies = [
NodeCreate(x=i % 10, y=0, z=i // 10)
for i in range(100)
]
result = await client.job.structure.nodes.bulk.post(bodies)
for node in result.succeeded or []:
print(f" Node {node.id}: ({node.x}, {node.y}, {node.z})")
for error in result.errors or []:
print(f" [{error.index}] {error.error}")
```
The same pattern applies to `Job.Structure.Members`,
`Job.Structure.Sections`, and the load primitives — every collection
endpoint has a `.Bulk` counterpart.
## Partial success with `continueOnError`
A bulk POST is all-or-nothing by default — if any item fails
validation, nothing is created and the call returns errors. Pass
`continueOnError=true` to commit every item that validates and report
the rest in `Errors`:
```csharp title="C#"
var result = await client.Job.Structure.Nodes.Bulk.PostAsync(
bodies,
config => config.QueryParameters.ContinueOnError = true);
Console.WriteLine($"Succeeded: {result!.Succeeded!.Count}");
Console.WriteLine($"Failed: {result.Errors!.Count}");
```
```python title="Python"
from kiota_abstractions.base_request_configuration import RequestConfiguration
from space_gass_api.job.structure.nodes.bulk.bulk_request_builder import BulkRequestBuilder
params = BulkRequestBuilder.BulkRequestBuilderPostQueryParameters(continue_on_error=True)
result = await client.job.structure.nodes.bulk.post(
bodies,
request_configuration=RequestConfiguration(query_parameters=params))
print(f"Succeeded: {len(result.succeeded or [])}")
print(f"Failed: {len(result.errors or [])}")
```
## When to use bulk
- Creating or updating more than one entity in a single script or
automated job.
- Importing or replicating a model from another source.
- Anywhere you'd otherwise loop a single-item call.
---
## Document: Switch to readonly mode
Release the SPACE GASS license seat so other users or instances can use it while you query results
URL: /docs/examples/switch-to-readonly
# Switch to readonly mode
The API runs in **readwrite** mode by default, holding a full SPACE GASS
license seat. If you only need to query results, switch to **readonly**
mode to release the seat — freeing it for the desktop application or
another API instance.
Readonly mode must be set **before** opening a job. If a job is already
open, you must close it first — the mode switch is queued until the job
closes.
## Switch to readonly, open, query, close
The snippet below switches to readonly, opens a sample project, queries
node reactions from a previous analysis, then closes the job. Because
the mode was set before opening, the API never acquires a SPACE GASS
license seat.
```csharp title="C#"
using SpaceGassApi;
using SpaceGassApi.Models;
var client = SpaceGassApiClient.CreateClient("http://localhost:34560");
// Switch to readonly before opening — no SPACE GASS seat consumed
await client.Service.AccessMode.PostAsync(
new AccessModeUpdate { AccessMode = AccessMode.ReadOnly });
try
{
await client.Job.OpenSample.PostAsync(
new OpenSampleRequest { FileName = "Portal Frame.SG" });
// Query existing analysis results (read-only operations only)
var reactions = await client.Job.Query.Analysis.Static.NodeReactions.GetAsync();
foreach (var r in reactions!.Results!)
{
Console.WriteLine($"Node {r.Node}, LC {r.LoadCase}: Fy={r.Fy:F2} kN");
}
}
finally
{
await client.Job.Close.PostAsync();
// Restore readwrite when finished
await client.Service.AccessMode.PostAsync(
new AccessModeUpdate { AccessMode = AccessMode.ReadWrite });
}
```
```python title="Python"
from space_gass_api import SpaceGassApiClient
import space_gass_api.models as models
client = SpaceGassApiClient.create_client("http://localhost:34560")
# Switch to readonly before opening — no SPACE GASS seat consumed
await client.service.access_mode.post(
models.AccessModeUpdate(access_mode=models.AccessMode.ReadOnly))
try:
await client.job.open_sample.post(
models.OpenSampleRequest(file_name="Portal Frame.SG"))
# Query existing analysis results (read-only operations only)
reactions = await client.job.query.analysis.static.node_reactions.get()
for r in reactions.results:
print(f"Node {r.node}, LC {r.load_case}: Fy={r.fy:.2f} kN")
finally:
await client.job.close.post()
# Restore readwrite when finished
await client.service.access_mode.post(
models.AccessModeUpdate(access_mode=models.AccessMode.ReadWrite))
```
```bash title="curl"
# Switch to readonly
curl -X POST http://localhost:34560/api/v1/service/access-mode \
-H "Content-Type: application/json" \
-d '{"accessMode": "ReadOnly"}'
# Open a sample (read-only — no SPACE GASS seat consumed)
curl -X POST http://localhost:34560/api/v1/job/open-sample \
-H "Content-Type: application/json" \
-d '{"fileName": "Portal Frame.SG"}'
# Query reactions
curl http://localhost:34560/api/v1/job/query/analysis/static/node-reactions
# Close and restore readwrite
curl -X POST http://localhost:34560/api/v1/job/close
curl -X POST http://localhost:34560/api/v1/service/access-mode \
-H "Content-Type: application/json" \
-d '{"accessMode": "ReadWrite"}'
```
### What if a job is already open?
If you call `POST /service/access-mode` with `"ReadOnly"` while a job is open,
the API returns **202 Accepted** — the transition is queued and commits
when the job closes. Close the job first if you need the mode to take
effect immediately.
If all license seats are in use when switching back to readwrite, the
API returns **409 Conflict** and stays in readonly.
### Check the current mode
Use `GET /service/access-mode` (the `accessMode` and `pendingAccessMode`
fields) to see the active mode and any pending transition.
## See also
- [Licensing](/licensing) — full explanation of readwrite and
readonly modes
- [Concepts — The Active Job](/concepts#the-active-job)
---
## Document: Simple Beam Model
Build a simple beam model, run analysis, and query results
URL: /docs/examples/simple-beam
# Simple Beam Model
This walkthrough demonstrates a complete SPACE GASS API workflow from
start to finish: create a project, build a simply-supported beam with
section, material, and loads, run a linear static analysis, and
retrieve the design results.
## What We Will Build
A simply-supported steel beam with self-weight, a dead load and a live
load, and ULS / SLS combinations to AS/NZS 1170:
- **Node 1** at (0, 0, 0) — fixed support
- **Node 2** at (6, 0, 0) — pinned support
- **Member 1** between the nodes, using a library section + library
material
- **Three primary load cases** — self-weight, dead, live
- **Two combination cases** — ULS strength, SLS deflection
- A linear static analysis run, then queries for the maximum ULS
bending moment and the SLS midspan deflection
## Project Setup
Before you can run this example you need a code editor and the SPACE GASS
SDK for your chosen language. If you already have a working .NET or
Python environment, skip ahead to [Step 1](#step-1--create-the-client-and-a-new-project).
We recommend [Visual Studio Code](https://code.visualstudio.com/) — it
is free, runs on Windows, macOS, and Linux, and has first-class support
for both C# and Python. If you already use Visual Studio, JetBrains
Rider, or PyCharm, those work too — the steps below translate directly.
### C#
1. Install the [.NET 8 SDK](https://dotnet.microsoft.com/download) and
[Visual Studio Code](https://code.visualstudio.com/).
2. In VS Code, install the
[C# Dev Kit](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit)
extension.
3. Pick a folder for your project (anywhere on disk), open it in VS Code
via **File → Open Folder...**, then open the integrated terminal with
**View → Terminal**. In the terminal, run:
```bash
dotnet new console
dotnet add package SpaceGassApi
```
4. Open `Program.cs` (created by the first command) and paste the example
code from the steps below. Run it with `dotnet run`.
### Python
1. Install [Python 3.10 or newer](https://www.python.org/downloads/) and
[Visual Studio Code](https://code.visualstudio.com/).
2. In VS Code, install the
[Python extension](https://marketplace.visualstudio.com/items?itemName=ms-python.python).
3. Pick a folder for your project (anywhere on disk), open it in VS Code
via **File → Open Folder...**, then open the integrated terminal with
**View → Terminal**. In the terminal, run:
```bash
python -m venv .venv
.venv\Scripts\activate
pip install space-gass-api
```
On macOS or Linux replace the second line with `source .venv/bin/activate`.
4. Create a new file called `main.py` (**File → New File**, save with
that name in the project folder) and paste the example code from the
steps below. Run it with `python main.py`.
## Step 1 — Create the Client and a New Project
`SpaceGassApiClient` is your handle to the running API. Every endpoint
follows the same pattern — `GetAsync` to read, `PostAsync` to create,
`PatchAsync` to update, `DeleteAsync` to remove. See the
[Using the SDK guide](/using-the-sdk) if these are new.
The API only has one project open at a time. After `Job.New` (or
`Job.Open` on an existing `.sg` file) the new project becomes the
**active** job — every later call in this walkthrough acts on it until
you `Job.Close` it in Step 16.
```csharp title="C#"
using SpaceGassApi;
using SpaceGassApi.Models;
var client = SpaceGassApiClient.CreateClient("http://localhost:34560");
// Create a new blank project
await client.Job.New.PostAsync();
```
```python title="Python"
from space_gass_api import SpaceGassApiClient
import space_gass_api.models as models
client = SpaceGassApiClient.create_client("http://localhost:34560")
# Create a new blank project
await client.job.new.post()
```
## Step 2 — Create Nodes
Create the two endpoints of the beam — Node 1 at the origin and Node 2
six metres along X. POSTing a `NodeCreate` to `Job.Structure.Nodes`
returns the saved node including the `Id` SPACE GASS allocated; hold
onto each Id, every later call that references this node uses it.
```csharp title="C#"
var node1 = await client.Job.Structure.Nodes.PostAsync(
new NodeCreate { X = 0.0, Y = 0.0, Z = 0.0 });
var node2 = await client.Job.Structure.Nodes.PostAsync(
new NodeCreate { X = 6.0, Y = 0.0, Z = 0.0 });
Console.WriteLine($"Node {node1.Id}: ({node1.X}, {node1.Y}, {node1.Z})");
Console.WriteLine($"Node {node2.Id}: ({node2.X}, {node2.Y}, {node2.Z})");
```
```python title="Python"
node1 = await client.job.structure.nodes.post(
models.NodeCreate(x=0.0, y=0.0, z=0.0))
node2 = await client.job.structure.nodes.post(
models.NodeCreate(x=6.0, y=0.0, z=0.0))
print(f"Node {node1.id}: ({node1.x}, {node1.y}, {node1.z})")
print(f"Node {node2.id}: ({node2.x}, {node2.y}, {node2.z})")
```
## Step 3 — Apply Restraints
Restraints are top-level entities — POST a `NodeRestraintCreate` (with
`Node` set on the body) to `Job.Structure.NodeRestraints`. The
6-character `RestraintCode` maps to the TX, TY, TZ, RX, RY, RZ DOFs.
Each character is one of:
| Code | Meaning |
| --- | --- |
| `F` | **Fixed** — prevents movement |
| `R` | **Released** — allows movement |
| `S` | Spring (movement governed by a spring stiffness) |
| `V` | Variable spring (stiffness-vs-deflection table) |
| `P` | Plastic (upper force / moment limit on the reaction) |
| `N` | Friction (limit proportional to the normal-axis reaction) |
For our beam, Node 1 is fully fixed (`FFFFFF` — all six DOFs prevented
from moving) and Node 2 is pinned (`FFFRRR` — translations fixed,
rotations released).
```csharp title="C#"
// Node 1: Fully fixed support (all 6 DOFs Fixed)
await client.Job.Structure.NodeRestraints.PostAsync(
new NodeRestraintCreate { Node = node1.Id, RestraintCode = "FFFFFF" });
// Node 2: Pinned support (translations Fixed, rotations Released)
await client.Job.Structure.NodeRestraints.PostAsync(
new NodeRestraintCreate { Node = node2.Id, RestraintCode = "FFFRRR" });
```
```python title="Python"
# Node 1: Fully fixed support (all 6 DOFs Fixed)
await client.job.structure.node_restraints.post(
models.NodeRestraintCreate(node=node1.id, restraint_code="FFFFFF"))
# Node 2: Pinned support (translations Fixed, rotations Released)
await client.job.structure.node_restraints.post(
models.NodeRestraintCreate(node=node2.id, restraint_code="FFFRRR"))
```
## Step 4 — Add a Material
Add the material before the member so its `Id` is available to assign.
POSTing a `MaterialLibraryCreate` to `Job.Structure.Materials.Library`
pulls a standard material from a SPACE GASS library — `Library` is the
library file name installed with SPACE GASS and `Name` is the material
designation in that library. (Pass a `MaterialUserCreate` to
`Job.Structure.Materials` directly if you need a fully user-defined
material instead.)
```csharp title="C#"
var steel = await client.Job.Structure.Materials.Library.PostAsync(
new MaterialLibraryCreate
{
Library = "Aust",
Name = "STEEL",
});
Console.WriteLine($"Material {steel.Id}: {steel.Name}");
```
```python title="Python"
steel = await client.job.structure.materials.library.post(
models.MaterialLibraryCreate(
library="Aust",
name="STEEL",
))
print(f"Material {steel.id}: {steel.name}")
```
## Step 5 — Add a Section
Like the material, create the section before the member so its `Id`
is available. POSTing a `SectionLibraryCreate` to
`Job.Structure.Sections.Library` pulls a standard profile from a SPACE
GASS library — `Library` is the library file name installed with SPACE
GASS and `Name` is the section designation in that library.
```csharp title="C#"
var section = await client.Job.Structure.Sections.Library.PostAsync(
new SectionLibraryCreate
{
Library = "Aust300",
Name = "360 UB 44.7",
Mark = "B1",
});
Console.WriteLine($"Section {section.Id}: {section.Name}");
```
```python title="Python"
section = await client.job.structure.sections.library.post(
models.SectionLibraryCreate(
library="Aust300",
name="360 UB 44.7",
mark="B1",
))
print(f"Section {section.id}: {section.name}")
```
## Step 6 — Create the Member
`MemberCreate` connects the two nodes with a single member and assigns
the section and material we just made. `NodeA` / `NodeB` take the Ids
from Step 2; `Section` and `Material` take the Ids returned by Steps 4
and 5.
```csharp title="C#"
var member = await client.Job.Structure.Members.PostAsync(
new MemberCreate
{
NodeA = node1.Id,
NodeB = node2.Id,
Section = section.Id,
Material = steel.Id,
});
Console.WriteLine($"Member {member.Id}: Node {member.NodeA} → Node {member.NodeB}");
```
```python title="Python"
member = await client.job.structure.members.post(
models.MemberCreate(
node_a=node1.id,
node_b=node2.id,
section=section.id,
material=steel.id,
))
print(f"Member {member.id}: Node {member.node_a} → Node {member.node_b}")
```
## Step 7 — Create Primary Load Cases
Create three primary cases up front — self-weight, dead, and live — so
their Ids are available when we attach loads in Steps 8–10. POSTing a
`LoadCaseCreate` to `Job.Loads.LoadCases` creates a primary case;
combination cases come later in Step 11.
```csharp title="C#"
var selfWeightCase = await client.Job.Loads.LoadCases.PostAsync(
new LoadCaseCreate { Id = 1, Title = "Self-weight" });
var deadCase = await client.Job.Loads.LoadCases.PostAsync(
new LoadCaseCreate { Id = 2, Title = "Dead Load" });
var liveCase = await client.Job.Loads.LoadCases.PostAsync(
new LoadCaseCreate { Id = 3, Title = "Live Load" });
Console.WriteLine($"Load cases: SW={selfWeightCase.Id}, G={deadCase.Id}, Q={liveCase.Id}");
```
```python title="Python"
self_weight_case = await client.job.loads.load_cases.post(
models.LoadCaseCreate(id=1, title="Self-weight"))
dead_case = await client.job.loads.load_cases.post(
models.LoadCaseCreate(id=2, title="Dead Load"))
live_case = await client.job.loads.load_cases.post(
models.LoadCaseCreate(id=3, title="Live Load"))
print(f"Load cases: SW={self_weight_case.id}, G={dead_case.id}, Q={live_case.id}")
```
## Step 8 — Apply the Self-Weight Load
Attach a self-weight load to the self-weight case. POST a
`SelfWeightLoadCreate` to `Job.Loads.SelfWeightLoads` with `Case` set
on the body — the load case must already exist, and one self-weight
load per case is allowed. Acceleration is expressed in **G** (multiples
of gravity), so set `AccelerationY = -1.0` for one G of gravity
in the negative-Y direction.
```csharp title="C#"
await client.Job.Loads.SelfWeightLoads.PostAsync(
new SelfWeightLoadCreate
{
LoadCase = selfWeightCase.Id,
AccelerationX = 0.0,
AccelerationY = -1.0, // 1 G downward
AccelerationZ = 0.0,
});
```
```python title="Python"
await client.job.loads.self_weight_loads.post(
models.SelfWeightLoadCreate(
load_case=self_weight_case.id,
acceleration_x=0.0,
acceleration_y=-1.0, # 1 G downward
acceleration_z=0.0,
))
```
## Step 9 — Add a Member Distributed Load to the Dead Case
Apply a uniform 2 kN/m downward across the full 6 m span on the dead
case. `MemberDistributedLoadCreate` takes the target `LoadCase` and
`Member` Ids, start / finish positions along the member (in length
units), and start / finish force intensity per unit length on each
axis.
```csharp title="C#"
await client.Job.Loads.MemberDistributedLoads.PostAsync(
new MemberDistributedLoadCreate
{
LoadCase = deadCase.Id,
Member = member.Id,
PositionUnits = LoadPositionUnits.Percent,
StartPosition = 0.0,
FinishPosition = 100.0,
FyStart = -2.0, // kN/m downward
FyFinish = -2.0,
});
```
```python title="Python"
await client.job.loads.member_distributed_loads.post(
models.MemberDistributedLoadCreate(
load_case=dead_case.id,
member=member.id,
position_units=models.LoadPositionUnits.Percent,
start_position=0.0,
finish_position=100.0,
fy_start=-2.0, # kN/m downward
fy_finish=-2.0,
))
```
## Step 10 — Add a Member Distributed Load to the Live Case
Same shape as Step 9 — swap the `LoadCase` to the live case and bump the
intensity to 5 kN/m.
```csharp title="C#"
await client.Job.Loads.MemberDistributedLoads.PostAsync(
new MemberDistributedLoadCreate
{
LoadCase = liveCase.Id,
Member = member.Id,
PositionUnits = LoadPositionUnits.Percent,
StartPosition = 0.0,
FinishPosition = 100.0,
FyStart = -5.0, // kN/m downward
FyFinish = -5.0,
});
```
```python title="Python"
await client.job.loads.member_distributed_loads.post(
models.MemberDistributedLoadCreate(
load_case=live_case.id,
member=member.id,
position_units=models.LoadPositionUnits.Percent,
start_position=0.0,
finish_position=100.0,
fy_start=-5.0, # kN/m downward
fy_finish=-5.0,
))
```
## Step 11 — Define the ULS and SLS Combinations
A combination case is created in a single call via
`Job.Loads.CombinationLoadCases`, with the title, Id, and the list of
items (primary case + factor pairs) supplied inline on
`CombinationLoadCaseCreate`. Below we define ULS (`1.2G + 1.5Q`) and SLS
short-term (`1.0G + 0.7Q`) combinations to AS/NZS 1170.
```csharp title="C#"
// ULS: 1.2 G + 1.5 Q (self-weight + dead are both G)
var ulsCase = await client.Job.Loads.CombinationLoadCases.PostAsync(
new CombinationLoadCaseCreate
{
Id = 10,
Title = "ULS - Strength",
CombinationItems = new List
{
new() { LoadCase = selfWeightCase.Id, MultiplyingFactor = 1.2 },
new() { LoadCase = deadCase.Id, MultiplyingFactor = 1.2 },
new() { LoadCase = liveCase.Id, MultiplyingFactor = 1.5 },
},
});
// SLS short-term: 1.0 G + 0.7 Q
var slsCase = await client.Job.Loads.CombinationLoadCases.PostAsync(
new CombinationLoadCaseCreate
{
Id = 20,
Title = "SLS - Short-term Deflection",
CombinationItems = new List
{
new() { LoadCase = selfWeightCase.Id, MultiplyingFactor = 1.0 },
new() { LoadCase = deadCase.Id, MultiplyingFactor = 1.0 },
new() { LoadCase = liveCase.Id, MultiplyingFactor = 0.7 },
},
});
```
```python title="Python"
# ULS: 1.2 G + 1.5 Q (self-weight + dead are both G)
uls_case = await client.job.loads.combination_load_cases.post(
models.CombinationLoadCaseCreate(
id=10,
title="ULS - Strength",
combination_items=[
models.CombinationLoadCaseItem(load_case=self_weight_case.id, multiplying_factor=1.2),
models.CombinationLoadCaseItem(load_case=dead_case.id, multiplying_factor=1.2),
models.CombinationLoadCaseItem(load_case=live_case.id, multiplying_factor=1.5),
],
))
# SLS short-term: 1.0 G + 0.7 Q
sls_case = await client.job.loads.combination_load_cases.post(
models.CombinationLoadCaseCreate(
id=20,
title="SLS - Short-term Deflection",
combination_items=[
models.CombinationLoadCaseItem(load_case=self_weight_case.id, multiplying_factor=1.0),
models.CombinationLoadCaseItem(load_case=dead_case.id, multiplying_factor=1.0),
models.CombinationLoadCaseItem(load_case=live_case.id, multiplying_factor=0.7),
],
))
```
## Step 12 — Save the Initial Model
Persist the project to disk before kicking off the analysis. If
something fails downstream, you can open the saved `.sg` in SPACE GASS
and inspect every entity, load, and combination to confirm the model
is set up the way you expect.
The `Save` response is a `JobStatus` whose `State.File` carries the
file's `Path`, `Name`, and `Source` — useful both as a sanity check
and for any tooling that needs to know where the file landed.
```csharp title="C#"
var initialSave = await client.Job.Save.PostAsync(
new SaveJobRequest { FilePath = @"C:\Projects\SimpleBeam.sg" });
var jobFile = initialSave?.State?.File;
Console.WriteLine($" Path: {jobFile?.Path}");
Console.WriteLine($" Name: {jobFile?.Name}");
Console.WriteLine($" Source: {jobFile?.Source}");
Console.WriteLine($" IsNew: {initialSave?.State?.IsNew}");
Console.WriteLine($" IsOpen: {initialSave?.State?.IsOpen}");
```
```python title="Python"
initial_save = await client.job.save.post(
models.SaveJobRequest(file_path="C:\\Projects\\SimpleBeam.sg"))
job_file = initial_save.state.file if initial_save and initial_save.state else None
print(f" Path: {job_file.path if job_file else None}")
print(f" Name: {job_file.name if job_file else None}")
print(f" Source: {job_file.source if job_file else None}")
print(f" IsNew: {initial_save.state.is_new if initial_save and initial_save.state else None}")
print(f" IsOpen: {initial_save.state.is_open if initial_save and initial_save.state else None}")
```
## Step 13 — Run a Linear Static Analysis
Analyses are **asynchronous** in SPACE GASS — when you start one, the
API returns immediately with a run Id and your code keeps going. Before
you can query results you need to wait until the run reaches a terminal
status (`Completed`, `Failed`, or `Cancelled`).
The simplest way is a poll loop, shown below. For longer analyses
where you want to display progress, see the
[Running Analysis guide](/guides/running-analysis).
```csharp title="C#"
// Start a linear static analysis. Pass an empty StaticSettingsUpdate
// to use the project's current settings as-is.
var run = await client.Job.Analysis.Static.RunLinear.PostAsync(
new StaticSettingsUpdate());
Console.WriteLine($"Run {run.RunId} queued; waiting for completion...");
// Poll until the run reaches a terminal state
AnalysisRun finalRun;
while (true)
{
await Task.Delay(500);
finalRun = await client.Job.Analysis.Runs[run.RunId.Value].GetAsync();
if (finalRun.Status is AnalysisRunStatus.Completed
or AnalysisRunStatus.Failed
or AnalysisRunStatus.Cancelled)
{
break;
}
}
Console.WriteLine($"Analysis {finalRun.Status} in {finalRun.ElapsedTime}");
if (finalRun.Status != AnalysisRunStatus.Completed)
{
throw new Exception($"Analysis did not complete: {finalRun.ErrorMessage}");
}
```
```python title="Python"
import asyncio
# Start a linear static analysis. Pass an empty models.StaticSettingsUpdate to
# use the project's current settings as-is.
run = await client.job.analysis.static.run_linear.post(
models.StaticSettingsUpdate())
print(f"Run {run.run_id} queued; waiting for completion...")
# Poll until the run reaches a terminal state
while True:
await asyncio.sleep(0.5)
final_run = await client.job.analysis.runs.by_run_id(str(run.run_id)).get()
if final_run.status in (
models.AnalysisRunStatus.Completed,
models.AnalysisRunStatus.Failed,
models.AnalysisRunStatus.Cancelled,
):
break
print(f"Analysis {final_run.status} in {final_run.elapsed_time}")
if final_run.status != models.AnalysisRunStatus.Completed:
raise RuntimeError(f"Analysis did not complete: {final_run.error_message}")
```
## Step 14 — Query Reactions
Read the support reactions under the ULS combination. By default the
query returns every result for every case; the `LoadCases` query parameter
filters to the load case(s) you want.
The result also carries a `Warnings` object. If a requested case (or
any of its constituent primaries, for combinations) has not been
analysed, its Id appears in `Warnings.LoadCasesNotAnalyzed` — check this
before reading `Results` so a missing run doesn't silently return zero
rows.
```csharp title="C#"
var reactions = await client.Job.Query.Analysis.Static.NodeReactions.GetAsync(
config => config.QueryParameters.LoadCases = $"{ulsCase.Id}");
if (reactions.Warnings?.LoadCasesNotAnalyzed is { Length: > 0 } missing)
{
throw new Exception($"Cases not analysed: {missing}. Run the analysis first.");
}
foreach (var r in reactions.Results)
{
Console.WriteLine(
$"Node {r.Node}, LC {r.LoadCase}: " +
$"Fx={r.Fx:F2}, Fy={r.Fy:F2}, Fz={r.Fz:F2}");
}
```
```python title="Python"
reactions = await client.job.query.analysis.static.node_reactions.get(
load_cases=str(uls_case.id))
if reactions.warnings and reactions.warnings.load_cases_not_analyzed:
raise RuntimeError(
f"Cases not analysed: {reactions.warnings.load_cases_not_analyzed}. "
"Run the analysis first.")
for r in reactions.results:
print(f"Node {r.node}, LC {r.load_case}: "
f"Fx={r.fx:.2f}, Fy={r.fy:.2f}, Fz={r.fz:.2f}")
```
## Step 15 — Get the Maximum ULS Bending Moment
Read the peak bending moment along the beam under the ULS combination.
The result is one row per (case, member) combination; force values are
returned as parallel arrays indexed by station, so element `[i]` of
every array describes the same point along the member:
```text
MemberIntermediateForce {
Case: int // load case Id
Member: int // member Id
Station: int[] // station index per sample
Location: float[] // distance along the member per sample
Fx, Fy, Fz: float[] // force per sample
Mx, My, Mz: float[] // moment per sample
}
```
Filter by ULS case + the member Id and take the maximum of `|Mz|`.
```csharp title="C#"
var ulsForces = await client.Job.Query.Analysis.Static.MemberIntermediateForces
.GetAsync(config =>
{
config.QueryParameters.LoadCases = $"{ulsCase.Id}";
config.QueryParameters.Members = $"{member.Id}";
});
var beamForces = ulsForces.Results.First();
var maxMz = beamForces.Mz.Max(v => Math.Abs(v ?? 0.0));
Console.WriteLine($"Max ULS bending moment on Member {member.Id}: {maxMz:F2} kNm");
```
```python title="Python"
uls_forces = await client.job.query.analysis.static.member_intermediate_forces.get(
load_cases=str(uls_case.id),
members=str(member.id))
beam_forces = uls_forces.results[0]
max_mz = max(abs(v) for v in beam_forces.mz if v is not None)
print(f"Max ULS bending moment on Member {member.id}: {max_mz:.2f} kNm")
```
## Step 16 — Get the SLS Midspan Deflection
Read the maximum vertical deflection along the beam under the SLS
combination. `MemberIntermediateDisplacement` follows the same columnar
shape as Step 14 — one row per (case, member), translations as
parallel arrays indexed by station:
```text
MemberIntermediateDisplacement {
Case: int // load case Id
Member: int // member Id
Station: int[] // station index per sample
Location: float[] // distance along the member per sample
TxLocal, TyLocal, TzLocal: float[] // translation in member-local axes
TxGlobal, TyGlobal, TzGlobal: float[] // translation in global axes
}
```
`TyGlobal` is the global-Y translation at each station; filter by SLS
case + the member Id and take the maximum of `|TyGlobal|`.
```csharp title="C#"
var slsDisplacements = await client.Job.Query.Analysis.Static.MemberIntermediateDisplacements
.GetAsync(config =>
{
config.QueryParameters.LoadCases = $"{slsCase.Id}";
config.QueryParameters.Members = $"{member.Id}";
});
var beamDisplacements = slsDisplacements.Results.First();
var maxDeflection = beamDisplacements.TyGlobal.Max(v => Math.Abs(v ?? 0.0));
Console.WriteLine($"Max SLS midspan deflection on Member {member.Id}: {maxDeflection * 1000:F2} mm");
```
```python title="Python"
sls_displacements = await client.job.query.analysis.static.member_intermediate_displacements.get(
load_cases=str(sls_case.id),
members=str(member.id))
beam_displacements = sls_displacements.results[0]
max_deflection = max(abs(v) for v in beam_displacements.ty_global if v is not None)
print(f"Max SLS midspan deflection on Member {member.id}: {max_deflection * 1000:.2f} mm")
```
## Step 17 — Save and Close
Save persists the project to disk so you can re-open it later from
SPACE GASS. Close ends the active job and releases the file. Run
`Job.Close` from a `finally` (C#) or `finally`-equivalent (Python)
block so the active job is always cleaned up — even if a step above
threw — leaving the service ready for the next run.
```csharp title="C#"
try
{
// ...all the steps above...
await client.Job.Save.PostAsync(
new SaveJobRequest { FilePath = @"C:\Projects\SimpleBeam.sg" });
}
finally
{
// Always close the active job so the next run starts clean.
try
{
await client.Job.Close.PostAsync();
}
catch (Exception closeEx)
{
Console.Error.WriteLine($"Warning: failed to close job: {closeEx.Message}");
}
}
```
```python title="Python"
try:
# ...all the steps above...
await client.job.save.post(
models.SaveJobRequest(file_path="C:\\Projects\\SimpleBeam.sg"))
finally:
# Always close the active job so the next run starts clean.
try:
await client.job.close.post()
except Exception as close_ex:
print(f"Warning: failed to close job: {close_ex}", file=sys.stderr)
```
## Run this example locally
The complete program with error handling is in the repo as
[`Example.CreateSimpleBeam`](https://github.com/SpaceGass/space-gass-api/tree/main/sdks/csharp/examples/Example.CreateSimpleBeam) (C#) and
[`create_simple_beam`](https://github.com/SpaceGass/space-gass-api/tree/main/sdks/python/examples/create_simple_beam) (Python).
Clone the repo and run it directly — no copy-paste:
```bash title="C#"
git clone https://github.com/SpaceGass/space-gass-api.git
cd space-gass-api/sdks/csharp/examples/Example.CreateSimpleBeam
dotnet run
```
```bash title="Python"
git clone https://github.com/SpaceGass/space-gass-api.git
cd space-gass-api/sdks/python/examples/create_simple_beam
pip install space-gass-api # once per environment
python create_simple_beam.py
```
The example saves the model to `~/Desktop/SpaceGass Examples/SimpleBeam.sg` so
you can open it in SPACE GASS Desktop and verify the geometry / results
visually.
---
## Document: Save and close cleanly
Persist the active job to disk, then release it with a try / finally so the close always runs
URL: /docs/examples/save-and-close
# Save and close cleanly
`Job.Save` writes the active job to a `.sg` file. The `filePath` works
like Save As — pass any path you want; the file is created (or
overwritten) at that location. Wrap the work in `try / finally` so
`Job.Close` runs even if a step in the middle throws, otherwise the
service is left holding an active job and the next `Open` will
return `409`.
The snippet below opens the `Portal Frame.SG` sample, saves it as a
new file on the desktop, and closes the job — useful as a template
for any "open something, save it as something else" workflow.
```csharp title="C#"
using SpaceGassApi;
using SpaceGassApi.Models;
var savePath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.Desktop),
"PortalFrameCopy.sg");
var client = SpaceGassApiClient.CreateClient("http://localhost:34560");
try
{
await client.Job.OpenSample.PostAsync(
new OpenSampleRequest { FileName = "Portal Frame.SG" });
// ...modify the model however you like...
await client.Job.Save.PostAsync(
new SaveJobRequest { FilePath = savePath });
Console.WriteLine($"Saved to {savePath}");
}
finally
{
await client.Job.Close.PostAsync();
}
```
```python title="Python"
import os
from space_gass_api import SpaceGassApiClient
import space_gass_api.models as models
save_path = os.path.join(
os.path.expanduser("~/Desktop"),
"PortalFrameCopy.sg",
)
client = SpaceGassApiClient.create_client("http://localhost:34560")
try:
await client.job.open_sample.post(
models.OpenSampleRequest(file_name="Portal Frame.SG"))
# ...modify the model however you like...
await client.job.save.post(
models.SaveJobRequest(file_path=save_path))
print(f"Saved to {save_path}")
finally:
await client.job.close.post()
```
```bash title="curl"
# Open the sample
curl -X POST http://localhost:34560/api/v1/job/open-sample \
-H "Content-Type: application/json" \
-d '{"fileName": "Portal Frame.SG"}'
# ...modify the model however you like...
curl -X POST http://localhost:34560/api/v1/job/save \
-H "Content-Type: application/json" \
-d '{"filePath": "C:\\Users\\you\\Desktop\\PortalFrameCopy.sg"}'
curl -X POST http://localhost:34560/api/v1/job/close
```
If you want to save to the path the job was originally opened from,
omit `filePath` — the API saves over the original. (Sample projects
opened with `Job.OpenSample` are unsaved and have no original path,
so you must supply one.)
## See also
- [File Handling](/guides/file-handling) — open, save, force-open,
status, and the full set of file lifecycle endpoints.
- [Concepts — The Active Job](/concepts#the-active-job)
---
## Document: Run a linear static analysis
Start a linear static run, poll for completion, and read the result status
URL: /docs/examples/run-linear-static-analysis
# Run a linear static analysis
Analysis is asynchronous — `POST /run-linear` starts it and returns a
`runId` immediately, then you poll `GET /runs/{runId}` until the
status reaches a terminal state (`Completed`, `Failed`, or `Cancelled`).
The snippet below opens your project, runs analysis with the current
settings, polls every half-second, and prints a one-line summary on
completion. The empty `StaticSettingsUpdate()` body means "use whatever
settings the job already has" — pass non-null fields to override,
including a selection of the load cases and combinations you would
like to run.
```csharp title="C#"
using SpaceGassApi;
using SpaceGassApi.Models;
var client = SpaceGassApiClient.CreateClient("http://localhost:34560");
try
{
await client.Job.OpenSample.PostAsync(
new OpenSampleRequest { FileName = "Portal Frame.SG" });
var run = await client.Job.Analysis.Static.RunLinear.PostAsync(
new StaticSettingsUpdate());
while (true)
{
await Task.Delay(500);
var status = await client.Job.Analysis.Runs[run!.RunId!.Value].GetAsync();
if (status!.Status is AnalysisRunStatus.Completed
or AnalysisRunStatus.Failed
or AnalysisRunStatus.Cancelled)
{
Console.WriteLine($"{status.Status} in {status.ElapsedTime}");
break;
}
}
}
finally
{
await client.Job.Close.PostAsync();
}
```
```python title="Python"
import asyncio
from space_gass_api import SpaceGassApiClient
import space_gass_api.models as models
client = SpaceGassApiClient.create_client("http://localhost:34560")
try:
await client.job.open_sample.post(
models.OpenSampleRequest(file_name="Portal Frame.SG"))
run = await client.job.analysis.static.run_linear.post(
models.StaticSettingsUpdate())
while True:
await asyncio.sleep(0.5)
status = await client.job.analysis.runs.by_run_id(str(run.run_id)).get()
if status.status in (
models.AnalysisRunStatus.Completed,
models.AnalysisRunStatus.Failed,
models.AnalysisRunStatus.Cancelled,
):
print(f"{status.status} in {status.elapsed_time}")
break
finally:
await client.job.close.post()
```
## Run this example locally
A richer version with progress polling, a reusable `WaitForCompletion`
helper, and a result summary is in the repo as
[`Example.RunAnalysis`](https://github.com/SpaceGass/space-gass-api/tree/main/sdks/csharp/examples/Example.RunAnalysis) (C#) and
[`run_analysis`](https://github.com/SpaceGass/space-gass-api/tree/main/sdks/python/examples/run_analysis) (Python).
Clone and run:
```bash title="C#"
git clone https://github.com/SpaceGass/space-gass-api.git
cd space-gass-api/sdks/csharp/examples/Example.RunAnalysis
dotnet run
```
```bash title="Python"
git clone https://github.com/SpaceGass/space-gass-api.git
cd space-gass-api/sdks/python/examples/run_analysis
pip install space-gass-api # once per environment
python run_analysis.py
```
Edit the example's `project_file_path` constant first to point at a
`.sg` file with structure and loads defined.
## See also
- [Running Analysis](/guides/running-analysis) — the full pattern with
a reusable `WaitForCompletion` helper and richer progress display.
- [Concepts — Analysis Runs](/concepts#analysis-runs)
---
## Document: Read reactions for restrained nodes only
Filter nodes to those with restraints, then read reactions for just those nodes
URL: /docs/examples/reactions-for-restrained-nodes
# Read reactions for restrained nodes only
Common after an analysis: you only care about reactions at supports,
not internal nodes. Two-step: filter nodes to `NodeType=Restrained`
to get the support node Ids, then pass them as the `Nodes` filter on
the reactions endpoint.
`Nodes` accepts SG list format — comma-separated Ids and dash ranges
(`"1,3-7,10"`). Build it with `string.Join(",", ids)` (C#) or `",".join(...)` (Python).
The snippet below opens the `Portal Frame.SG` sample, runs a linear
static analysis to populate reactions, queries restrained nodes, then
prints their reactions. `try / finally` closes the job even if a step
in the middle throws.
```csharp title="C#"
using SpaceGassApi;
using SpaceGassApi.Models;
var client = SpaceGassApiClient.CreateClient("http://localhost:34560");
try
{
await client.Job.OpenSample.PostAsync(
new OpenSampleRequest { FileName = "Portal Frame.SG" });
// Run analysis so reactions are available.
var run = await client.Job.Analysis.Static.RunLinear.PostAsync(
new StaticSettingsUpdate());
while (true)
{
await Task.Delay(500);
var s = await client.Job.Analysis.Runs[run!.RunId!.Value].GetAsync();
if (s!.Status is AnalysisRunStatus.Completed
or AnalysisRunStatus.Failed
or AnalysisRunStatus.Cancelled) break;
}
// 1. Get the restrained nodes
var restrained = await client.Job.Structure.Nodes.GetAsync(
config => config.QueryParameters.NodeType = NodeTypeFilter.Restrained);
var nodeFilter = string.Join(",",
restrained!.Where(n => n.Id != null).Select(n => n.Id!.Value));
// 2. Read reactions filtered to those nodes
var result = await client.Job.Query.Analysis.Static.NodeReactions.GetAsync(
config => config.QueryParameters.Nodes = nodeFilter);
foreach (var r in result!.Results!)
{
Console.WriteLine(
$"Node {r.Node}, Case {r.LoadCase}: " +
$"Fx={r.Fx:F2}, Fy={r.Fy:F2}, Fz={r.Fz:F2}");
}
}
finally
{
await client.Job.Close.PostAsync();
}
```
```python title="Python"
import asyncio
from space_gass_api import SpaceGassApiClient
import space_gass_api.models as models
client = SpaceGassApiClient.create_client("http://localhost:34560")
try:
await client.job.open_sample.post(
models.OpenSampleRequest(file_name="Portal Frame.SG"))
# Run analysis so reactions are available.
run = await client.job.analysis.static.run_linear.post(
models.StaticSettingsUpdate())
while True:
await asyncio.sleep(0.5)
s = await client.job.analysis.runs.by_run_id(str(run.run_id)).get()
if s.status in (
models.AnalysisRunStatus.Completed,
models.AnalysisRunStatus.Failed,
models.AnalysisRunStatus.Cancelled,
):
break
# 1. Get the restrained nodes
restrained = await client.job.structure.nodes.get(
node_type=models.NodeTypeFilter.Restrained)
node_filter = ",".join(str(n.id) for n in restrained if n.id is not None)
# 2. Read reactions filtered to those nodes
result = await client.job.query.analysis.static.node_reactions.get(
nodes=node_filter)
for r in result.results:
print(f"Node {r.node}, Case {r.load_case}: "
f"Fx={r.fx:.2f}, Fy={r.fy:.2f}, Fz={r.fz:.2f}")
finally:
await client.job.close.post()
```
```bash title="curl"
# 1. Restrained nodes
curl "http://localhost:34560/api/v1/job/structure/nodes?nodeType=Restrained"
# 2. Reactions for the returned Ids (substitute below)
curl "http://localhost:34560/api/v1/job/query/analysis/static/node-reactions?nodes=1,2,3"
```
## Run this example locally
The same code (against your own .sg file rather than the sample) is
in the repo as
[`Example.QueryRestrainedNodes`](https://github.com/SpaceGass/space-gass-api/tree/main/sdks/csharp/examples/Example.QueryRestrainedNodes) (C#) and
[`query_restrained_nodes`](https://github.com/SpaceGass/space-gass-api/tree/main/sdks/python/examples/query_restrained_nodes) (Python).
Clone and run:
```bash title="C#"
git clone https://github.com/SpaceGass/space-gass-api.git
cd space-gass-api/sdks/csharp/examples/Example.QueryRestrainedNodes
dotnet run
```
```bash title="Python"
git clone https://github.com/SpaceGass/space-gass-api.git
cd space-gass-api/sdks/python/examples/query_restrained_nodes
pip install space-gass-api # once per environment
python query_restrained_nodes.py
```
Edit the example's `PROJECT_FILE_PATH` constant first to point at an
analysed `.sg` file.
## See also
- [Filtering & Querying](/guides/filtering-and-querying) — full SG-list
filter syntax and pagination.
---
## Document: Open your own .sg file
Load a SPACE GASS project from disk into the active job
URL: /docs/examples/open-your-own-file
# Open your own .sg file
The [Quick Start](/quick-start) uses `Job.OpenSample`
to load a built-in sample so you don't need a project file. Once you
have your own `.sg` file, switch to `Job.Open` — same shape, but you
supply the path.
## Check the file status first
Before opening, call `File.Status` to find out whether the file is in
a safe state. The response includes `CanOpenSafely` (a simple boolean),
a human-readable `Description`, and a `RecommendedAction` string you
can show to the user if something is wrong.
```csharp title="C#"
using SpaceGassApi;
using SpaceGassApi.Models;
var client = SpaceGassApiClient.CreateClient("http://localhost:34560");
var status = await client.File.Status.GetAsync(
config => config.QueryParameters.FilePath = @"C:\Projects\MyStructure.sg");
Console.WriteLine($"Status: {status!.Status}");
Console.WriteLine($"Description: {status.Description}");
Console.WriteLine($"Can open safely: {status.CanOpenSafely}");
if (status.CanOpenSafely != true)
{
Console.WriteLine($"Recommended action: {status.RecommendedAction}");
}
```
```python title="Python"
from space_gass_api import SpaceGassApiClient
import space_gass_api.models as models
client = SpaceGassApiClient.create_client("http://localhost:34560")
status = await client.file.status.get(
file_path=r"C:\Projects\MyStructure.sg")
print(f"Status: {status.status}")
print(f"Description: {status.description}")
print(f"Can open safely: {status.can_open_safely}")
if not status.can_open_safely:
print(f"Recommended action: {status.recommended_action}")
```
```bash title="curl"
curl "http://localhost:34560/api/v1/file/status?filePath=C%3A%5CProjects%5CMyStructure.sg"
```
## Open the file
`Job.Open` returns `409 Conflict` if a job is already open — close it
first with `Job.Close`. If the file has unsaved temporary files from a
previous crashed session, set `ForceOption` on the request body to
either `OpenPreviousSaved` (discard the unsaved changes) or
`OpenUnsavedMostRecent` (recover them).
```csharp title="C#"
using SpaceGassApi;
using SpaceGassApi.Models;
var client = SpaceGassApiClient.CreateClient("http://localhost:34560");
try
{
await client.Job.Open.PostAsync(
new OpenJobRequest { FilePath = @"C:\Projects\MyStructure.sg" });
// ...do work on the open job...
}
finally
{
await client.Job.Close.PostAsync();
}
```
```python title="Python"
from space_gass_api import SpaceGassApiClient
import space_gass_api.models as models
client = SpaceGassApiClient.create_client("http://localhost:34560")
try:
await client.job.open.post(
models.OpenJobRequest(file_path=r"C:\Projects\MyStructure.sg"))
# ...do work on the open job...
finally:
await client.job.close.post()
```
```bash title="curl"
curl -X POST http://localhost:34560/api/v1/job/open \
-H "Content-Type: application/json" \
-d '{"filePath": "C:\\Projects\\MyStructure.sg"}'
# ...calls against the open job...
curl -X POST http://localhost:34560/api/v1/job/close
```
## See also
- [Concepts — The Active Job](/concepts#the-active-job)
- [File Handling](/guides/file-handling) — full set of open / save /
status patterns.
---
## Document: Filter analysis results by load case
Read reactions or member forces for one specific case (or set of cases) using SG list format
URL: /docs/examples/filter-results-by-case
# Filter analysis results by load case
Every result endpoint accepts a `LoadCases` query parameter to restrict
the response to a subset of analysed cases. The format is SG's list
syntax — comma-separated Ids and dash ranges in a single string:
| Filter | Meaning |
|---|---|
| `"1"` | Just case 1 |
| `"1,3,7"` | Cases 1, 3, and 7 |
| `"1,3-7,10"` | Case 1, 3 through 7 inclusive, and 10 |
| `""` or omit | All analysed cases |
The same format applies to `Nodes`, `Members`, `Modes`, and any other
list filter.
The snippet below opens the `Portal Frame.SG` sample, runs a linear
static analysis, then reads node reactions filtered to load case `1`.
Adjust the case Id to match a case that exists in your model.
`try / finally` closes the job at the end even if a step in the middle
throws.
```csharp title="C#"
using SpaceGassApi;
using SpaceGassApi.Models;
var client = SpaceGassApiClient.CreateClient("http://localhost:34560");
try
{
await client.Job.OpenSample.PostAsync(
new OpenSampleRequest { FileName = "Portal Frame.SG" });
var run = await client.Job.Analysis.Static.RunLinear.PostAsync(
new StaticSettingsUpdate());
while (true)
{
await Task.Delay(500);
var s = await client.Job.Analysis.Runs[run!.RunId!.Value].GetAsync();
if (s!.Status is AnalysisRunStatus.Completed
or AnalysisRunStatus.Failed
or AnalysisRunStatus.Cancelled) break;
}
var reactions = await client.Job.Query.Analysis.Static.NodeReactions.GetAsync(
config => config.QueryParameters.LoadCases = "1");
if (reactions!.Warnings?.LoadCasesNotAnalyzed is { Length: > 0 } missing)
{
throw new Exception($"Cases not analysed: {missing}. Run the analysis first.");
}
foreach (var r in reactions.Results!)
{
Console.WriteLine($"Node {r.Node}: Fx={r.Fx:F2}, Fy={r.Fy:F2}, Fz={r.Fz:F2}");
}
}
finally
{
await client.Job.Close.PostAsync();
}
```
```python title="Python"
import asyncio
from space_gass_api import SpaceGassApiClient
import space_gass_api.models as models
client = SpaceGassApiClient.create_client("http://localhost:34560")
try:
await client.job.open_sample.post(
models.OpenSampleRequest(file_name="Portal Frame.SG"))
run = await client.job.analysis.static.run_linear.post(
models.StaticSettingsUpdate())
while True:
await asyncio.sleep(0.5)
s = await client.job.analysis.runs.by_run_id(str(run.run_id)).get()
if s.status in (
models.AnalysisRunStatus.Completed,
models.AnalysisRunStatus.Failed,
models.AnalysisRunStatus.Cancelled,
):
break
reactions = await client.job.query.analysis.static.node_reactions.get(
load_cases="1")
if reactions.warnings and reactions.warnings.load_cases_not_analyzed:
raise RuntimeError(
f"Cases not analysed: {reactions.warnings.load_cases_not_analyzed}. "
"Run the analysis first.")
for r in reactions.results:
print(f"Node {r.node}: Fx={r.fx:.2f}, Fy={r.fy:.2f}, Fz={r.fz:.2f}")
finally:
await client.job.close.post()
```
```bash title="curl"
# Single case
curl "http://localhost:34560/api/v1/job/query/analysis/static/node-reactions?loadCases=10"
# Multiple cases via SG list
curl "http://localhost:34560/api/v1/job/query/analysis/static/node-reactions?loadCases=1,3-7,10"
```
### Always check `Warnings.LoadCasesNotAnalyzed`
If you ask for a case that hasn't been analysed (or, for a combination,
any of its primary cases), it appears in `Warnings.LoadCasesNotAnalyzed`
and silently returns no rows for that case. Check the warning before
reading `Results` so a missing run doesn't look like clean data.
## See also
- [Filtering & Querying](/guides/filtering-and-querying)
- [Concepts — Analysis Runs](/concepts#analysis-runs)