Skip to main content

Handlers

A Handler is the HTTP face of your app. When Atlan needs to test connection credentials, run preflight checks, or discover available metadata, it sends HTTP requests to your app's handler service. The Handler receives those requests, validates the incoming data with typed contracts, performs the work, and returns a structured response.

The division of responsibility is straightforward:

  • The App handles durable execution—it runs tasks that extract, transform, or publish metadata, and automatically recovers from failures.
  • The Handler handles the HTTP protocol—it validates credentials, checks connectivity, and surfaces metadata for the app configuration UI.

Both run as separate services in production, but share the same codebase and can run together in a single process during local development.

Implement handler

Every handler subclasses Handler from application_sdk.handler and implements three methods:

from application_sdk.handler import Handler
from application_sdk.handler.contracts import (
AuthInput, AuthOutput, AuthStatus,
PreflightInput, PreflightOutput, PreflightStatus,
MetadataInput, MetadataOutput,
)

class MyHandler(Handler):
async def test_auth(self, input: AuthInput) -> AuthOutput:
...

async def preflight_check(self, input: PreflightInput) -> PreflightOutput:
...

async def fetch_metadata(self, input: MetadataInput) -> MetadataOutput:
...

Each method receives a single typed Input and returns a single typed Output. The framework validates both at the boundary—malformed requests are rejected before your code runs.

The framework injects self.context before each method call and clears it after, making handlers stateless and safe for concurrent requests.

Methods

Each method receives a single typed Input and returns a typed Output. Raise a typed error from application_sdk.errors for protocol failures—the framework maps it to the appropriate HTTP status automatically. Pydantic ValidationError on the request body is automatically caught and returned as 400.

Error classHTTP statusWhen to use
InvalidInputError400Missing or malformed credentials/config in the request body
AuthError401Credentials present but invalid; token expired
AppPermissionDeniedError403Auth succeeded but the role lacks required permissions
NotFoundError404Target database, schema, or resource doesn't exist
RateLimitedError429Request throttled by the target system
DependencyUnavailableError503Target is unreachable (network down, DNS failure)
AppTimeoutError504Request exceeded its timeout
InternalError500Genuine SDK or connector bug (last resort)

test_auth

Tests whether the provided credentials are valid.

class AuthInput:
credentials: list[HandlerCredential] = [] # key/value credential pairs
connection_id: str = "" # optional connection ID
timeout_seconds: int = 30

class AuthOutput:
status: AuthStatus # SUCCESS, FAILED, EXPIRED, or INVALID_CREDENTIALS
message: str = "" # optional detail visible to the user
identities: list[str] = [] # verified identities (usernames, roles)
scopes: list[str] = [] # authorized scopes or permissions
expires_at: str = "" # ISO-8601 expiry timestamp

When credentials are present but authentication fails (wrong password, expired token, account locked), return AuthOutput with status=AuthStatus.FAILED rather than raising. This gives the UI a message it can display directly to the user:

async def test_auth(self, input: AuthInput) -> AuthOutput:
try:
async with connect(...) as conn:
await conn.execute("SELECT 1")
return AuthOutput(status=AuthStatus.SUCCESS)
except AuthenticationError as exc:
return AuthOutput(
status=AuthStatus.FAILED,
message=f"Connection failed: {exc}",
)

preflight_check

Runs one or more named connectivity checks before an app runs.

class PreflightInput:
credentials: list[HandlerCredential] = []
connection_config: dict[str, Any] = {} # host, port, database, etc.
checks_to_run: list[str] = [] # empty list = run all checks
timeout_seconds: int = 60

class PreflightOutput:
status: PreflightStatus # READY, NOT_READY, or PARTIAL
checks: list[PreflightCheck] = [] # individual check results
message: str = ""
total_duration_ms: float = 0.0

class PreflightCheck:
name: str # machine-readable check identifier
passed: bool
message: str = ""

Raise typed errors to give callers semantic HTTP status codes they can act on without parsing message strings:

from application_sdk.errors import (
AppPermissionDeniedError,
DependencyUnavailableError,
InvalidInputError,
)

class MyHandler(Handler):
async def preflight_check(self, input: PreflightInput) -> PreflightOutput:
if not self.context.has_credential("api_key"):
raise InvalidInputError("api_key credential is required")

try:
async with connect(self.context.get_credential("host")) as conn:
await conn.execute("SELECT 1")
except ConnectionRefusedError as exc:
raise DependencyUnavailableError("Warehouse is unreachable") from exc
except PermissionDeniedError as exc:
raise AppPermissionDeniedError("Role lacks SELECT on information_schema") from exc

return PreflightOutput(status=PreflightStatus.READY)

When some checks pass and others fail, return PreflightStatus.PARTIAL so the UI can show the user exactly what's and isn't working:

async def preflight_check(self, input: PreflightInput) -> PreflightOutput:
checks = [...] # run each check independently
passed = sum(1 for c in checks if c.passed)
if passed == len(checks):
status = PreflightStatus.READY
elif passed == 0:
status = PreflightStatus.NOT_READY
else:
status = PreflightStatus.PARTIAL
return PreflightOutput(status=status, checks=checks)

fetch_metadata

Returns the set of schemas or objects available for selection in the Atlan app configuration UI. Two specialized output types are provided for the most common cases:

class MetadataInput:
credentials: list[HandlerCredential] = []
connection_config: dict[str, Any] = {}
object_filter: str = "" # filter pattern, e.g. "public.*"
include_fields: bool = True
max_objects: int = 1000
timeout_seconds: int = 120

# For SQL connectors (populates the sqltree widget)
class SqlMetadataOutput(MetadataOutput):
objects: list[SqlMetadataObject] = []

class SqlMetadataObject:
TABLE_CATALOG: str = ""
TABLE_SCHEMA: str = ""

# For API connectors (populates the apitree widget)
class ApiMetadataOutput(MetadataOutput):
objects: list[ApiMetadataObject] = []

Context in handlers

self.context is injected per request and cleared after—it's stateless across requests. It provides access to the credentials submitted in the request body and to the framework's secret store. This is the same self.context interface available inside @task methods on an App.

Credentials

Credentials arrive as key/value pairs in the request body. Access them by key:

async def test_auth(self, input: AuthInput) -> AuthOutput:
# Check whether a key is present before using it
if not self.context.has_credential("api_key"):
raise InvalidInputError("api_key is required")

# Retrieve a specific credential value
api_key = self.context.get_credential("api_key")

# Access all credentials as a list
all_creds = self.context.credentials

Other secrets

For secrets not tied to request credentials, use get_secret:

async def test_auth(self, input: AuthInput) -> AuthOutput:
signing_key = await self.context.get_secret("my-signing-key")

Full example

The following handler validates credentials by attempting a real connection, runs two named preflight checks, and returns SQL schema objects for the metadata browser:

from application_sdk.handler import Handler
from application_sdk.handler.contracts import (
AuthInput, AuthOutput, AuthStatus,
PreflightInput, PreflightOutput, PreflightStatus, PreflightCheck,
MetadataInput, SqlMetadataOutput, SqlMetadataObject,
)
from application_sdk.errors import InvalidInputError


class WarehouseHandler(Handler):
async def test_auth(self, input: AuthInput) -> AuthOutput:
host = self.context.get_credential("host")
username = self.context.get_credential("username")
password = self.context.get_credential("password")

if not host or not username or not password:
raise InvalidInputError("host, username, and password credentials are required")

try:
async with create_connection(host, username, password) as conn:
await conn.execute("SELECT 1")
return AuthOutput(status=AuthStatus.SUCCESS)
except Exception as exc:
return AuthOutput(
status=AuthStatus.FAILED,
message=f"Connection failed: {exc}",
)

async def preflight_check(self, input: PreflightInput) -> PreflightOutput:
host = self.context.get_credential("host")
username = self.context.get_credential("username")
password = self.context.get_credential("password")

schema_ok = False
tables_ok = False
try:
async with create_connection(host, username, password) as conn:
schemas = await conn.execute(
"SELECT COUNT(*) FROM information_schema.schemata"
)
schema_ok = True
tables = await conn.execute(
"SELECT COUNT(*) FROM information_schema.tables"
)
tables_ok = True
except Exception:
pass

checks = [
PreflightCheck(
name="schemaCheck",
passed=schema_ok,
message="Schema access check successful" if schema_ok else "Could not query schemas",
),
PreflightCheck(
name="tableCheck",
passed=tables_ok,
message="Table access check successful" if tables_ok else "Could not query tables",
),
]
status = PreflightStatus.READY if all(c.passed for c in checks) else PreflightStatus.NOT_READY
return PreflightOutput(status=status, checks=checks)

async def fetch_metadata(self, input: MetadataInput) -> SqlMetadataOutput:
host = self.context.get_credential("host")
username = self.context.get_credential("username")
password = self.context.get_credential("password")

async with create_connection(host, username, password) as conn:
rows = await conn.execute(
"SELECT TABLE_CATALOG, TABLE_SCHEMA FROM information_schema.schemata"
)
return SqlMetadataOutput(
objects=[
SqlMetadataObject(
TABLE_CATALOG=row["TABLE_CATALOG"],
TABLE_SCHEMA=row["TABLE_SCHEMA"],
)
for row in rows
]
)

See also

  • Credentials: typed credential references and self.context.resolve_credential()
  • Apps and tasks: how the App and its tasks work alongside the Handler
  • Built-in APIs: full API reference for handler contracts and the built-in server layer