Back to Blog

Custom Function Development Guide

Custom Functions are the core mechanism for extending computation capabilities in the coomia-dip platform. Write functions in Python, register them with the platform, and reuse business logic across Actions, Pipelines, Rules, and Dashboards. This guide covers the complete workflow: function definition, registration, testing, version management, and production deployment.

CoomiaPublished on January 17, 202611 min read
Share this articleTwitter / X

Series: S12 Developer Tutorials · Article 8 | Level: Intermediate | Reading Time: 15 min

Custom Function Development Guide

#TL;DR

Custom Functions are the core mechanism for extending computation capabilities in the coomia-dip platform. Write functions in Python, register them with the platform, and reuse business logic across Actions, Pipelines, Rules, and Dashboards. This guide covers the complete workflow: function definition, registration, testing, version management, and production deployment.

#1. Function Overview

#1.1 What is a Custom Function?

A Custom Function is a Python code unit running on the Intelligence Layer (Reasoning & Decision Layer) that can be invoked by other components in the Ontology ecosystem. Functions support both synchronous and asynchronous modes and are exposed as platform services via gRPC.

Code
┌─────────────┐    gRPC    ┌──────────────────┐
│ Action      │ ────────── │                  │
│ Pipeline    │ ────────── │  Function Runtime │
│ Rule Engine │ ────────── │  (Reasoning & Decision Layer)        │
│ Dashboard   │ ────────── │                  │
└─────────────┘            └──────────────────┘

#1.2 Function Types

TypePurposeExamples
ComputeData transformation and calculationRisk scoring, pricing strategy
QueryComplex data queriesCross-object aggregation, graph traversal
IntegrationExternal system integrationAPI calls, message push
ValidationData validationBusiness rule validation, format checking

#2. Environment Setup

#2.1 Project Structure

Code
my_functions/
├── pyproject.toml
├── py.typed
├── __init__.py
├── functions/
│   ├── __init__.py
│   ├── risk_scoring.py
│   ├── notification.py
│   └── data_validation.py
├── models/
│   ├── __init__.py
│   └── schemas.py
└── tests/
    ├── __init__.py
    ├── test_risk_scoring.py
    └── test_notification.py

#2.2 Dependency Configuration

TOML
# pyproject.toml
[project]
name = "my-onto-functions"
version = "1.0.0"
requires-python = ">=3.11"

dependencies = [
    "ontology-sdk>=1.0.0",
    "pydantic>=2.0",
]

[project.optional-dependencies]
dev = [
    "pytest>=7.0",
    "pytest-asyncio>=0.21",
    "black",
    "ruff",
    "mypy",
]

#3. Writing Your First Function

#3.1 Basic Function Definition

Python
# functions/risk_scoring.py
from ontology_sdk.functions import function, FunctionContext
from pydantic import BaseModel, Field
from typing import Optional
from enum import Enum


class RiskLevel(str, Enum):
    LOW = "low"
    MEDIUM = "medium"
    HIGH = "high"
    CRITICAL = "critical"


class RiskInput(BaseModel):
    """Risk scoring input parameters"""
    customer_rid: str = Field(description="Customer object RID")
    assessment_type: str = Field(default="standard", description="Assessment type")


class RiskOutput(BaseModel):
    """Risk scoring output result"""
    risk_level: RiskLevel
    risk_score: float = Field(ge=0, le=100)
    risk_factors: list[str]
    recommendations: list[str]
    confidence: float = Field(ge=0, le=1)


@function(
    name="calculate_risk_score",
    display_name="Customer Risk Score",
    description="Calculate risk score based on customer transaction history and behavior data",
    version="1.0.0",
    input_type=RiskInput,
    output_type=RiskOutput,
    tags=["risk", "scoring", "customer"],
)
def calculate_risk_score(ctx: FunctionContext, input: RiskInput) -> RiskOutput:
    """Calculate customer risk score"""

    # Fetch customer data
    customer = ctx.platform.objects.get_by_rid(input.customer_rid)
    if not customer:
        raise ValueError(f"Customer not found: {input.customer_rid}")

    # Fetch transaction history
    transactions = ctx.platform.oql.execute(f"""
        FIND Transaction
        TRAVERSE belongs_to -> Customer
        WHERE Customer.rid = '{input.customer_rid}'
        SELECT amount, transaction_date, type, status
        ORDER BY transaction_date DESC
        LIMIT 100
    """)

    # Calculate risk factors
    risk_factors = []
    score = 50.0  # Baseline score

    # Factor 1: Abnormal transaction frequency
    recent_count = sum(1 for t in transactions
                       if (ctx.now() - t.transaction_date).days < 7)
    if recent_count > 20:
        score += 15
        risk_factors.append("Abnormally high transaction frequency in last 7 days")

    # Factor 2: Large transaction ratio
    large_txns = [t for t in transactions if t.amount > 100000]
    if len(large_txns) / max(len(transactions), 1) > 0.3:
        score += 20
        risk_factors.append("Large transactions exceed 30% of total")

    # Factor 3: Failed transaction rate
    failed = [t for t in transactions if t.status == "failed"]
    fail_rate = len(failed) / max(len(transactions), 1)
    if fail_rate > 0.1:
        score += 10
        risk_factors.append(f"Transaction failure rate: {fail_rate:.1%}")

    # Factor 4: Customer tier
    if customer.tier == "bronze":
        score += 5
        risk_factors.append("Low-tier customer")

    # Determine risk level
    score = min(100, max(0, score))
    if score >= 80:
        level = RiskLevel.CRITICAL
    elif score >= 60:
        level = RiskLevel.HIGH
    elif score >= 40:
        level = RiskLevel.MEDIUM
    else:
        level = RiskLevel.LOW

    # Generate recommendations
    recommendations = []
    if level in (RiskLevel.CRITICAL, RiskLevel.HIGH):
        recommendations.append("Manual review of recent transactions recommended")
        recommendations.append("Enable enhanced identity verification")
    if "large transaction" in str(risk_factors).lower():
        recommendations.append("Enable dual approval for large transactions")

    return RiskOutput(
        risk_level=level,
        risk_score=score,
        risk_factors=risk_factors,
        recommendations=recommendations,
        confidence=0.85,
    )

#3.2 Async Functions

Python
# functions/notification.py
from ontology_sdk.functions import async_function, FunctionContext
from pydantic import BaseModel, Field
from typing import Optional
import httpx


class NotificationInput(BaseModel):
    channel: str = Field(description="Notification channel: email/slack/webhook")
    recipients: list[str] = Field(description="Recipient list")
    subject: str
    body: str
    template: Optional[str] = None
    priority: str = Field(default="normal")


class NotificationOutput(BaseModel):
    sent_count: int
    failed_count: int
    failures: list[dict] = Field(default_factory=list)


@async_function(
    name="send_notification",
    display_name="Send Notification",
    description="Send notification messages via specified channel",
    version="1.0.0",
    input_type=NotificationInput,
    output_type=NotificationOutput,
    timeout_seconds=30,
    retry_on_failure=True,
    max_retries=3,
)
async def send_notification(
    ctx: FunctionContext, input: NotificationInput
) -> NotificationOutput:
    """Send notifications to specified channels"""

    sent = 0
    failed = 0
    failures = []

    # Render template
    body = input.body
    if input.template:
        template = await ctx.platform.templates.get(input.template)
        body = template.render(body=input.body, subject=input.subject)

    async with httpx.AsyncClient() as client:
        for recipient in input.recipients:
            try:
                if input.channel == "email":
                    await _send_email(client, recipient, input.subject, body)
                elif input.channel == "slack":
                    await _send_slack(client, recipient, input.subject, body)
                elif input.channel == "webhook":
                    await _send_webhook(client, recipient, input.subject, body)
                sent += 1
            except Exception as e:
                failed += 1
                failures.append({
                    "recipient": recipient,
                    "error": str(e)
                })
                ctx.logger.warning(f"Failed to send to {recipient}: {e}")

    return NotificationOutput(
        sent_count=sent,
        failed_count=failed,
        failures=failures,
    )


async def _send_email(client, recipient, subject, body):
    await client.post(
        "http://mail-service:8080/api/send",
        json={"to": recipient, "subject": subject, "body": body}
    )


async def _send_slack(client, recipient, subject, body):
    await client.post(
        recipient,
        json={"text": f"*{subject}*\n{body}"}
    )


async def _send_webhook(client, recipient, subject, body):
    await client.post(recipient, json={"subject": subject, "body": body})

#4. Function Registration and Management

#4.1 Registering Functions

Python
from ontology_sdk import OntoPlatform

platform = OntoPlatform(
    control_plane_url="localhost:50051",
    intelligence_plane_url="localhost:50053"
)

# Register from module
platform.functions.register_module("functions.risk_scoring")
platform.functions.register_module("functions.notification")

# Batch register from directory
platform.functions.register_directory("functions/")

# List registered functions
for fn in platform.functions.list():
    print(f"{fn.name} v{fn.version} - {fn.display_name}")
    print(f"  Input: {fn.input_schema}")
    print(f"  Output: {fn.output_schema}")

#4.2 Invoking Functions

Python
# Synchronous invocation
result = platform.functions.invoke(
    "calculate_risk_score",
    input={
        "customer_rid": "ri.ontology.object.customer.c-001",
        "assessment_type": "enhanced"
    }
)
print(f"Risk level: {result.risk_level}")
print(f"Risk score: {result.risk_score}")

# Asynchronous invocation
import asyncio

async def main():
    result = await platform.functions.invoke_async(
        "send_notification",
        input={
            "channel": "slack",
            "recipients": ["#risk-alerts"],
            "subject": "Risk Alert",
            "body": "Customer XX risk score has abnormally increased"
        }
    )
    print(f"Sent: {result.sent_count}, Failed: {result.failed_count}")

asyncio.run(main())

# Batch invocation
batch_results = platform.functions.invoke_batch(
    "calculate_risk_score",
    inputs=[
        {"customer_rid": f"ri.ontology.object.customer.c-{i:03d}"}
        for i in range(1, 101)
    ],
    concurrency=10
)

#5. Testing Functions

#5.1 Unit Tests

Python
# tests/test_risk_scoring.py
import pytest
from unittest.mock import MagicMock
from datetime import timedelta
from functions.risk_scoring import calculate_risk_score, RiskInput, RiskLevel
from ontology_sdk.functions import FunctionContext


@pytest.fixture
def mock_context():
    ctx = MagicMock(spec=FunctionContext)

    # Mock customer data
    customer = MagicMock()
    customer.tier = "gold"
    customer.annual_revenue = 5000000
    ctx.platform.objects.get_by_rid.return_value = customer

    # Mock transaction data
    ctx.platform.oql.execute.return_value = [
        MagicMock(amount=50000, transaction_date=ctx.now() - timedelta(days=1),
                  type="purchase", status="success"),
        MagicMock(amount=200000, transaction_date=ctx.now() - timedelta(days=3),
                  type="purchase", status="success"),
    ]

    return ctx


class TestRiskScoring:
    def test_low_risk_customer(self, mock_context):
        """Low risk customer should return LOW level"""
        input_data = RiskInput(
            customer_rid="ri.ontology.object.customer.c-001"
        )

        result = calculate_risk_score(mock_context, input_data)

        assert result.risk_level == RiskLevel.LOW
        assert result.risk_score < 40
        assert result.confidence > 0

    def test_high_risk_customer(self, mock_context):
        """Customer with many large transactions should return high risk"""
        from datetime import timedelta
        now = mock_context.now()
        mock_context.platform.oql.execute.return_value = [
            MagicMock(amount=500000, transaction_date=now - timedelta(days=i),
                      type="purchase", status="success")
            for i in range(25)
        ] + [
            MagicMock(amount=100, transaction_date=now - timedelta(days=i),
                      type="purchase", status="failed")
            for i in range(5)
        ]

        input_data = RiskInput(
            customer_rid="ri.ontology.object.customer.c-002"
        )

        result = calculate_risk_score(mock_context, input_data)

        assert result.risk_level in (RiskLevel.HIGH, RiskLevel.CRITICAL)
        assert result.risk_score >= 60
        assert len(result.risk_factors) > 0
        assert len(result.recommendations) > 0

    def test_customer_not_found(self, mock_context):
        """Non-existent customer should raise exception"""
        mock_context.platform.objects.get_by_rid.return_value = None

        input_data = RiskInput(
            customer_rid="ri.ontology.object.customer.nonexistent"
        )

        with pytest.raises(ValueError, match="Customer not found"):
            calculate_risk_score(mock_context, input_data)

#5.2 Integration Tests

Python
# tests/test_integration.py
import pytest
from ontology_sdk.testing import TestPlatform


@pytest.fixture
def test_platform():
    """Use test platform (in-memory mode)"""
    return TestPlatform(
        seed_data={
            "Customer": [
                {"rid": "c-001", "name": "Test Customer A", "tier": "gold"},
                {"rid": "c-002", "name": "Test Customer B", "tier": "bronze"},
            ],
            "Transaction": [
                {"customer_rid": "c-001", "amount": 50000, "status": "success"},
                {"customer_rid": "c-001", "amount": 200000, "status": "success"},
                {"customer_rid": "c-002", "amount": 500000, "status": "failed"},
            ]
        }
    )


class TestRiskScoringIntegration:
    def test_end_to_end(self, test_platform):
        """End-to-end integration test"""
        result = test_platform.functions.invoke(
            "calculate_risk_score",
            input={"customer_rid": "c-001"}
        )

        assert result.risk_level is not None
        assert 0 <= result.risk_score <= 100

#6. Accessing Ontology from Functions

#6.1 Reading Objects

Python
@function(name="enrich_project_data", ...)
def enrich_project_data(ctx: FunctionContext, input: dict) -> dict:
    # Get object by RID
    project = ctx.platform.objects.get_by_rid(input["project_rid"])

    # Find by filter
    dept = ctx.platform.objects.get(
        "Department",
        filters={"code": project.department_code}
    )

    # OQL query
    team_members = ctx.platform.oql.execute(f"""
        FIND Employee
        TRAVERSE participates_in -> Project
        WHERE Project.rid = '{project.rid}'
        SELECT Employee.name, Employee.level, participates_in.role
    """)

    return {
        "project_name": project.name,
        "department": dept.name if dept else "Unknown",
        "team_size": len(team_members),
        "team": [{"name": m.name, "role": m.role} for m in team_members],
    }

#6.2 Writing Objects

Python
@function(name="create_project_report", ...)
def create_project_report(ctx: FunctionContext, input: dict) -> dict:
    # Create new object
    report = ctx.platform.objects.create(
        object_type="Report",
        properties={
            "title": f"Monthly Report - {input['month']}",
            "project_rid": input["project_rid"],
            "generated_at": ctx.now().isoformat(),
            "content": generate_report_content(ctx, input),
        }
    )

    # Create relation
    ctx.platform.relations.create(
        relation_type="has_report",
        source_rid=input["project_rid"],
        target_rid=report.rid,
    )

    return {"report_rid": report.rid, "status": "created"}

#7. Function Versioning and Hot Updates

#7.1 Version Management

Python
# Register new version
@function(name="calculate_risk_score", version="2.0.0", ...)
def calculate_risk_score_v2(ctx, input):
    # New version logic
    pass

# List versions
versions = platform.functions.list_versions("calculate_risk_score")
for v in versions:
    print(f"v{v.version}: {v.status} (deployed {v.deployed_at})")

# Canary release
platform.functions.set_traffic_split("calculate_risk_score", {
    "1.0.0": 80,  # 80% traffic to old version
    "2.0.0": 20,  # 20% traffic to new version
})

# Full promotion
platform.functions.promote("calculate_risk_score", version="2.0.0")

# Rollback
platform.functions.rollback("calculate_risk_score", target_version="1.0.0")

#8. Using Functions in Other Components

#8.1 In Actions

YAML
# actions/assess_customer_risk.yaml
name: assess_customer_risk
type: function_call
function: calculate_risk_score
input_mapping:
  customer_rid: $trigger.object_rid
output_handling:
  - condition: output.risk_level == 'critical'
    action: send_notification
    params:
      channel: slack
      subject: "High-Risk Customer Alert"
      body: "Customer ${trigger.object.name} risk score: ${output.risk_score}"

#8.2 In Pipeline Transforms

YAML
transform:
  stages:
    - name: risk_enrichment
      type: function_call
      function: calculate_risk_score
      input_mapping:
        customer_rid: $record.customer_rid
      output_mapping:
        risk_level: $output.risk_level
        risk_score: $output.risk_score

#8.3 In Dashboards

Python
@function(name="dashboard_project_health", ...)
def dashboard_project_health(ctx: FunctionContext, input: dict) -> dict:
    projects = ctx.platform.oql.execute("""
        FIND Project WHERE status = 'in_progress'
        SELECT name, budget, end_date, priority
    """)

    return {
        "total_projects": len(projects),
        "by_priority": {
            "critical": len([p for p in projects if p.priority == "critical"]),
            "high": len([p for p in projects if p.priority == "high"]),
            "medium": len([p for p in projects if p.priority == "medium"]),
            "low": len([p for p in projects if p.priority == "low"]),
        },
        "total_budget": sum(p.budget for p in projects),
    }

#9. Performance Optimization

#9.1 Caching Strategy

Python
from ontology_sdk.functions import function, FunctionContext


@function(name="get_department_info", cache_ttl=300, ...)
def get_department_info(ctx: FunctionContext, input: dict) -> dict:
    """Department info query with caching (TTL 5 minutes)"""
    dept = ctx.platform.objects.get("Department", filters={"code": input["dept_code"]})
    return {"name": dept.name, "location": dept.location, "head_count": dept.head_count}

#9.2 Concurrency Control

Python
@function(
    name="batch_risk_assessment",
    concurrency_limit=20,
    timeout_seconds=60,
    ...
)
def batch_risk_assessment(ctx: FunctionContext, input: dict) -> dict:
    """Batch risk assessment with concurrency limited to 20"""
    results = []
    for customer_rid in input["customer_rids"]:
        result = ctx.invoke("calculate_risk_score", {"customer_rid": customer_rid})
        results.append(result)
    return {"assessments": results}

#10. Complete Example: Building an Approval Function

Python
from ontology_sdk.functions import function, FunctionContext
from pydantic import BaseModel, Field
from typing import Optional


class ApprovalInput(BaseModel):
    request_rid: str
    approver_rid: str
    decision: str  # approve / reject
    comment: Optional[str] = None


class ApprovalOutput(BaseModel):
    status: str
    next_approver: Optional[str] = None
    is_final: bool
    audit_trail_rid: str


@function(
    name="process_approval",
    display_name="Process Approval",
    description="Process approval request, supporting multi-level approval workflows",
    version="1.0.0",
    input_type=ApprovalInput,
    output_type=ApprovalOutput,
)
def process_approval(ctx: FunctionContext, input: ApprovalInput) -> ApprovalOutput:
    # Fetch approval request
    request = ctx.platform.objects.get_by_rid(input.request_rid)
    if not request:
        raise ValueError(f"Approval request not found: {input.request_rid}")

    # Verify approver permission
    if request.current_approver_rid != input.approver_rid:
        raise PermissionError("Current user is not the designated approver")

    # Record audit trail
    audit = ctx.platform.objects.create(
        object_type="AuditTrail",
        properties={
            "request_rid": input.request_rid,
            "approver_rid": input.approver_rid,
            "decision": input.decision,
            "comment": input.comment,
            "timestamp": ctx.now().isoformat(),
        }
    )

    # Handle decision
    if input.decision == "reject":
        ctx.platform.objects.update(
            rid=input.request_rid,
            properties={"status": "rejected", "completed_at": ctx.now().isoformat()}
        )
        return ApprovalOutput(
            status="rejected",
            is_final=True,
            audit_trail_rid=audit.rid,
        )

    # Find next approver in chain
    approval_chain = ctx.platform.oql.execute(f"""
        FIND ApprovalStep
        TRAVERSE belongs_to -> ApprovalFlow
        WHERE ApprovalFlow.type = '{request.flow_type}'
        SELECT step_order, approver_role, min_amount
        ORDER BY step_order ASC
    """)

    next_step = next(
        (s for s in approval_chain if s.step_order == request.current_step + 1), None
    )

    if next_step and request.amount >= next_step.min_amount:
        next_approver = ctx.platform.oql.execute(f"""
            FIND Employee WHERE role = '{next_step.approver_role}' LIMIT 1
        """)

        ctx.platform.objects.update(
            rid=input.request_rid,
            properties={
                "current_step": request.current_step + 1,
                "current_approver_rid": next_approver[0].rid if next_approver else None,
                "status": "pending_approval",
            }
        )

        return ApprovalOutput(
            status="pending_next_approval",
            next_approver=next_approver[0].rid if next_approver else None,
            is_final=False,
            audit_trail_rid=audit.rid,
        )
    else:
        ctx.platform.objects.update(
            rid=input.request_rid,
            properties={"status": "approved", "completed_at": ctx.now().isoformat()}
        )
        return ApprovalOutput(
            status="approved",
            is_final=True,
            audit_trail_rid=audit.rid,
        )

#Key Takeaways

  1. Functions as a Service: Custom functions are exposed via gRPC, callable from Actions, Pipelines, Rules, and Dashboards
  2. Type safety: Use Pydantic v2 to define input/output schemas with type validation
  3. Testability: Unit test with Mock Context, integration test with TestPlatform
  4. Version management: Support multi-version coexistence, canary releases, and fast rollback
  5. Performance control: Built-in caching, concurrency limits, and timeout mechanisms
  6. Ontology-native: Functions can directly access objects, relations, and OQL queries

#Next Article

Next: S12-09 Metric Development Guide — Learn how to define and compute business metrics and build a metric system.

Tags: Custom Functions Python gRPC Functions as a Service Pydantic coomia-dip