Writing OFS Functions
Project structure
Section titled “Project structure”my-function/├── aerostack.json # OFS manifest (required)├── src/│ └── index.ts # Function entry point├── tests/│ └── index.test.ts # Optional tests└── package.jsonFunction signature
Section titled “Function signature”export default async function( inputs: Record<string, any>, // Validated against your manifest's input schema context: { sdk: AerostackSDK, // Injected by the runtime user?: User, // Present when auth: "required" or "optional" env: Env, // Runtime environment variables meta: { projectId: string, functionName: string, } }): Promise<Record<string, any>> // Validated against output schemaExample: user profile function
Section titled “Example: user profile function”{ "name": "get-user-profile", "version": "1.0.0", "description": "Get a user's profile with cached lookups", "runtime": "any", "inputs": { "userId": { "type": "string", "required": true } }, "outputs": { "user": { "type": "object", "required": true }, "fromCache": { "type": "boolean" } }, "requires": ["db", "cache"], "auth": "required"}export default async function({ userId }, { sdk, user }) { // Only return your own profile (or admin can see any) if (userId !== user.id && !user.customFields?.isAdmin) { throw new Error('Forbidden') }
const cacheKey = `user:${userId}`
const cached = await sdk.cache.get(cacheKey) if (cached) return { user: cached, fromCache: true }
const profile = await sdk.db.queryOne( 'SELECT id, email, name, avatar_url, created_at FROM users WHERE id = ?', [userId] )
if (!profile) throw new Error('User not found')
await sdk.cache.set(cacheKey, profile, { ttl: 300 })
return { user: profile, fromCache: false }}Input validation
Section titled “Input validation”OFS validates inputs against the manifest schema before your function runs. Invalid inputs throw a 400 VALIDATION_ERROR before your code executes:
"inputs": { "email": { "type": "string", "required": true, "format": "email" }, "age": { "type": "number", "min": 0, "max": 150 }, "role": { "type": "string", "enum": ["admin", "user", "viewer"] }, "tags": { "type": "array", "items": { "type": "string" } }}Error handling
Section titled “Error handling”Throw plain errors — OFS wraps them in a standard response:
throw new Error('User not found')// → 404 { "error": "USER_NOT_FOUND", "message": "User not found" }
throw Object.assign(new Error('Rate limit exceeded'), { code: 'RATE_LIMITED', status: 429 })// → 429 { "error": "RATE_LIMITED", "message": "Rate limit exceeded" }Testing locally
Section titled “Testing locally”import { testFunction } from '@aerostack/test-utils'import fn from '../src/index'
test('returns user profile', async () => { const result = await testFunction(fn, { inputs: { userId: 'test-user-id' }, mockSDK: { db: { queryOne: () => ({ id: 'test-user-id', email: 'test@example.com' }) }, cache: { get: () => null, set: () => {} } }, user: { id: 'test-user-id' } })
expect(result.user.email).toBe('test@example.com') expect(result.fromCache).toBe(false)})Run locally
Section titled “Run locally”aerostack dev# → Starts a local OFS runner on http://localhost:8787# → POST http://localhost:8787/ with JSON body to invoke your function