Agent-native job state management API service for async APIs.
Define your request/response payload models and create the Flask app:
from sandjig import create_app
from sandjig.models import RequestPostPayloadBaseModel, ResponsePostPayloadBaseModel
class MyRequestPostPayload(RequestPostPayloadBaseModel):
examplevalue: str
class MyResponsePostPayload(ResponsePostPayloadBaseModel):
result_value: int
app = create_app(MyRequestPostPayload, MyResponsePostPayload, config={})Import DynamoDB-backed job models from the correctly spelled module path:
from sandjig.jobsapi.dynamodb.models import ProcessingJobModelThe previous typo path, sandjig.jobsapi.dyanmodb, has been removed. This is a
breaking change — update any imports to sandjig.jobsapi.dynamodb.
from sandjig import create_app
from sandjig.models import RequestPostPayloadBaseModel, ResponsePostPayloadBaseModel, SettingsBaseModel
class MyRequestPostPayload(RequestPostPayloadBaseModel):
examplevalue: str
class MyResponsePostPayload(ResponsePostPayloadBaseModel):
result_value: int
class MySettings(SettingsBaseModel):
adjust: float = 0.5
app = create_app(MyRequestPostPayload, MyResponsePostPayload, MySettings, config={})export AWS_PROFILE={profile}
export AWS_DEFAULT_REGION={region}
export BASIC_AUTH_USERNAME={username}
export BASIC_AUTH_PASSWORD={password}
sandjig deploy -s {SUFFIX} -n {APP_PYTHON_FILE} --stage {stg|dev|prd}If
ENDPOINT_PREFIXis set in config, it prepends/jobsand/settingsendpoints. Example:ENDPOINT_PREFIX="/api"produces/api/jobs.
POST /jobs- Submit a new jobGET /jobs- List jobs (paginated)GET /jobs/{JOB_ID}- Get job statusPATCH /jobs/{JOB_ID}- Update job status
| Parameter | Description |
|---|---|
limit |
Items per page (50-500, default 250) |
job_id |
Comma-separated UUIDs to filter (max 175) |
status |
Filter by status (pending, queued, validating, processing, completed, error, cancelled) |
registered_datetime_gte |
ISO-8601 datetime lower bound |
registered_datetime_lte |
ISO-8601 datetime upper bound |
Only available when a SettingsBaseModel subclass is passed to create_app().
GET /settingsPATCH /settings
GET /openapi- Swagger UIGET /openapi/schema- OpenAPI YAML specGET /healthcheck- 200 OK health check
{
"job_id": "{JOB_ID}",
"registered_datetime": "2026-04-16T14:53:18+09:00",
"updated_datetime": "2026-04-16T14:53:20+09:00",
"completed_datetime": null,
"status": "pending",
"result_count": 0,
"settings": null,
"request_payload": {
"examplevalue": "my example"
},
"response_payload": null
}| Field | Description |
|---|---|
API_TITLE |
Display title for OpenAPI UI |
API_VERSION |
OpenAPI displayed version |
BASIC_AUTH_FORCE |
When True, basic auth is required. BASIC_AUTH_USERNAME and BASIC_AUTH_PASSWORD env vars must be set. |
BASIC_AUTH_USERNAME |
Username for basic auth (when BASIC_AUTH_FORCE=True) |
BASIC_AUTH_PASSWORD |
Password for basic auth (when BASIC_AUTH_FORCE=True) |
SQS_QUEUE_URL |
If set, job requests are sent as messages to this SQS queue |
ENDPOINT_PREFIX |
Prefix for /jobs endpoints (must start with /) |
JOBREQUEST_CALLBACK_FUNCTION |
Callable invoked on successful job request with job_id argument |
JOBREQUEST_AUTHORIZATION_FUNCTION |
Callable invoked with the decoded POST /jobs payload before validation/enqueue. Return None to allow the request, or a Flask response (e.g. ({"message": "unauthorized"}, 401)) to reject it. |
JOBREQUEST_TRANSFORM_FUNCTION |
Callable invoked with the decoded POST /jobs payload after authorization and before enqueue. Returns the payload to enqueue -- server-injected fields override client-supplied values and are validated against the request model before enqueue. NOTE: the raw client payload is spec-validated first, so fields the server injects must be declared optional on the request model. |
JSON_AS_ASCII |
If True, JSON dumped as ASCII (default False) |
Enforce host-app authentication and inject the server-side user_id (Flask request context is available inside the hooks):
from http import HTTPStatus
from flask import session
def require_login(payload: dict) -> tuple[dict, int] | None:
if "user_id" not in session:
return {"message": "authentication required"}, HTTPStatus.UNAUTHORIZED
return None # allow
def inject_user_id(payload: dict) -> dict:
payload["user_id"] = session["user_id"] # overrides any client-supplied value
return payload
config = {
"JOBREQUEST_AUTHORIZATION_FUNCTION": require_login,
"JOBREQUEST_TRANSFORM_FUNCTION": inject_user_id,
}
app = create_app(MyRequestModel, MyResponseModel, config=config)sandjig deploy [-h] [-b BUCKET] [--stage STAGE] -n APPNAME
sandjig update [-h] [-b BUCKET] [--stage STAGE] -n APPNAME
sandjig destroy [-h]
sandjig package [-h] [--stage STAGE] -n APPNAME -o OUTPUT_DIRECTORY
sandjig template [-h] [-o OUTPUT]sandjig pins Python to >=3.14,<3.15 and deploys to the AWS Lambda python3.14
runtime (ValidPythonRuntimes in sandjig/definitions.py; SAM parameter
PythonRuntime). Embedding applications must therefore run on Python 3.14 —
match requires-python = ">=3.14,<3.15" in your own pyproject.toml, and use a
python3.14 runtime or python:3.14 container base image when deploying your
app Lambda yourself (resources-only mode).
When AWS ships a newer runtime, add it to ValidPythonRuntimes and widen the
pin in the same change so the package and the deploy targets stay in sync.
Python: 3.14
Requires uv for dependency management
-
Install
pre-commithooks:pre-commit install
-
Install project and development dependencies:
uv sync
uv run poe checkuv run poe typecheckRequires localstack (docker-compose):
docker compose up -d
uv run poe testRequired for local development (.env):
AWS_ACCOUNT_ID=dummyid
S3_SERVICE_ENDPOINT=http://localhost:4566
SQS_SERVICE_ENDPOINT=http://localhost:4566
STS_SERVICE_ENDPOINT=http://localhost:4566
DYNAMODB_SERVICE_ENDPOINT=http://localhost:4566
Optional runtime configuration:
| Variable | Description |
|---|---|
RESPONSE_TIMEZONE |
IANA timezone applied to JobResponse datetimes (e.g. UTC, America/Los_Angeles). Default Asia/Tokyo (+09:00) |