Connectors¶
A connector is a bidirectional adapter between Mycel and an external system. Every connector can act as a source (receives data that triggers a flow) or a target (destination where a flow writes data). Some are naturally one-directional — email is output-only, cron is input-only — but most work both ways.
Connector Table¶
| Type | Driver / Examples | As Source | As Target |
|---|---|---|---|
rest |
HTTP server | Expose endpoints | — |
http |
HTTP client | — | Call APIs |
database |
postgres, mysql, sqlite, mongodb |
Query data | Insert/Update/Delete |
graphql |
GraphQL server/client | Expose schema | Query/Mutate |
mq |
rabbitmq, kafka, redis |
Consume messages | Publish messages |
grpc |
gRPC server/client | Expose services | Call services |
tcp |
TCP server/client | Receive connections | Send data |
cache |
memory, redis |
— | Read/write cache |
file |
Local filesystem | Watch for files | Write files |
s3 |
AWS S3, MinIO | Read objects | Write objects |
websocket |
WebSocket server | Receive client events | Push to clients |
sse |
Server-Sent Events | Connect/disconnect events | Push events |
cdc |
PostgreSQL WAL | Stream DB changes | — |
exec |
Shell commands | — | Execute commands |
email |
SMTP | — | Send emails |
slack |
Slack API | — | Send messages |
discord |
Discord API | — | Send messages |
sms |
Twilio | — | Send SMS |
push |
FCM, APNs | — | Push notifications |
webhook |
HTTP callbacks | — | Send webhooks |
soap |
SOAP 1.1/1.2 | Expose SOAP endpoints | Call SOAP services |
elasticsearch |
Elasticsearch | — | Index/Search |
oauth |
Google, GitHub, Apple, OIDC | OAuth callback | — |
mqtt |
MQTT 3.1.1/5.0 | Subscribe to topics | Publish messages |
ftp |
FTP, FTPS, SFTP | List/Download files | Upload/Delete files |
pdf |
PDF generation | — | Render documents |
Defining a Connector¶
The connector name is how flows reference it: connector = "NAME" in a flow's from or to block.
Common Connectors¶
REST Server¶
connector "api" {
type = "rest"
port = 3000
cors {
origins = ["*"]
methods = ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
headers = ["Content-Type", "Authorization"]
}
}
HTTP Client¶
connector "external_api" {
type = "http"
base_url = "https://api.example.com"
timeout = "30s"
auth {
type = "bearer"
token = env("API_TOKEN")
}
retry {
attempts = 3 # total tries, including the first
delay = "1s" # wait before the second try
backoff = "exponential" # constant | linear | exponential
max_delay = "30s" # cap however far the wait grows
}
}
Database¶
# PostgreSQL
connector "db" {
type = "database"
driver = "postgres"
host = env("PG_HOST")
port = 5432
database = env("PG_DATABASE")
user = env("PG_USER")
password = env("PG_PASSWORD")
ssl_mode = "require"
pool {
max = 100
min = 10
max_lifetime = 300
}
}
# MySQL
connector "mysql" {
type = "database"
driver = "mysql"
host = env("MYSQL_HOST")
port = 3306
database = env("MYSQL_DATABASE")
user = env("MYSQL_USER")
password = env("MYSQL_PASSWORD")
}
# SQLite (no server needed)
connector "local_db" {
type = "database"
driver = "sqlite"
database = "./data.db"
}
# MongoDB
connector "mongo" {
type = "database"
driver = "mongodb"
uri = env("MONGO_URI")
database = "myapp"
}
Message Queue¶
# RabbitMQ
connector "rabbit" {
type = "mq"
driver = "rabbitmq"
host = env("RABBITMQ_HOST")
port = 5672
username = "guest"
password = env("RABBITMQ_PASS")
vhost = "/"
}
# Kafka
connector "kafka" {
type = "mq"
driver = "kafka"
brokers = ["kafka:9092"]
}
# Redis Pub/Sub
connector "redis_events" {
type = "mq"
driver = "redis"
url = env("REDIS_URL", "redis://localhost:6379")
channels = ["orders", "payments"]
}
Cache¶
# Redis
connector "cache" {
type = "cache"
driver = "redis"
url = env("REDIS_URL", "redis://localhost:6379")
default_ttl = "1h"
prefix = "myapp:"
}
# In-memory (no external service)
connector "local_cache" {
type = "cache"
driver = "memory"
max_items = 10000
eviction = "lru"
}
GraphQL¶
# Server
connector "gql" {
type = "graphql"
driver = "server"
port = 4000
endpoint = "/graphql"
playground = true
subscriptions {
enabled = true
path = "/graphql/ws" # default /subscriptions
keep_alive_interval = "30s" # ping period on an idle socket
connection_timeout = "60s" # drop a connection that stops answering
# A subscription is idle by nature, so the server pings it: without that
# the socket looks dead to every proxy between here and the client, and
# the first one with an idle timeout closes it while the client still
# believes it is subscribed. connection_timeout is always given room for
# an answer — a timeout at or below the ping period is widened.
}
}
# Client
connector "external_gql" {
type = "graphql"
driver = "client"
endpoint = "https://api.example.com/graphql"
timeout = "30s"
}
gRPC¶
# Server
connector "grpc_api" {
type = "grpc"
driver = "server"
port = 50051
proto_path = "./proto"
proto_files = ["user.proto", "order.proto"]
reflection = true
}
# Client
connector "user_service" {
type = "grpc"
driver = "client"
target = "users-service:50051"
proto_path = "./proto"
proto_files = ["user.proto"]
insecure = false
wait_for_ready = true
}
File System¶
connector "files" {
type = "file"
base_path = "./data"
format = "json"
create_dirs = true
# Enable file watching (triggers flows on new/modified files)
watch = true
watch_interval = "5s"
}
S3¶
connector "storage" {
type = "s3"
bucket = env("S3_BUCKET")
region = env("AWS_REGION")
# For MinIO or custom S3-compatible
endpoint = env("S3_ENDPOINT")
access_key = env("S3_ACCESS_KEY")
secret_key = env("S3_SECRET_KEY")
use_path_style = true
}
use_path_style addresses objects as endpoint/bucket/key instead of
bucket.endpoint/key, which MinIO and most S3-compatible stores require. If you
are arriving from the AWS SDK v1 or an older Terraform provider, this is the
setting they call force_path_style.
Named Operations¶
Named operations define reusable parameterized queries on a connector. Instead of repeating SQL or API call patterns across flows, define them once and reference them by name.
connector "db" {
type = "database"
driver = "postgres"
# ... connection details
operation "find_active_users" {
query = "SELECT * FROM users WHERE status = 'active' AND org_id = $1"
description = "Active users for one organisation"
param "org_id" {
type = "string"
required = true
description = "Organisation to filter by"
}
}
operation "list_recent" {
query = "SELECT * FROM users ORDER BY created_at DESC LIMIT $1"
param "limit" {
type = "number"
default = 100
}
}
}
Each parameter is its own param block, named by its label. The block is a
contract, applied before the flow runs: defaults fill in what was not sent, and
what was sent is converted to the declared type and checked against the
constraints.
| Attribute | Applies to | Description |
|---|---|---|
type |
any | string, number, boolean, array or object. A value that can be converted is — see below. |
required |
any | Reject the request when the parameter is absent and no default covers it. |
default |
any | Value used when the parameter is not supplied. A parameter with a default is never missing. |
in |
any | Where the value comes from: path, query, header or body. |
min, max |
numbers | Smallest and largest allowed value. |
min_length, max_length |
strings | Shortest and longest allowed value. |
pattern |
strings | Regular expression the value must match. |
enum |
strings | The complete set of allowed values. |
description |
any | Documentation, carried into the exported OpenAPI spec. |
connector "api" {
type = "rest"
port = 8080
operation "search_users" {
method = "GET"
path = "/users"
param "limit" {
type = "number"
default = 100
min = 1
max = 500
}
param "sort" {
type = "string"
enum = ["name", "email", "created_at"]
default = "name"
}
param "tenant" {
type = "string"
required = true
min_length = 3
}
}
}
GET /users?limit=600 is answered with 400 and
invalid parameters: limit: value must be at most 500, before the flow runs.
Every problem in a request is reported at once, so a caller is not made to fix
one per round trip.
The declared type converts. Path and query parameters arrive as strings —
always — so type = "number" would reject every request that uses it if it
were enforced literally. ?limit=25 reaches the flow as the number 25, and
?limit=abc is a 400 naming the parameter. The same applies to boolean,
which accepts true and false as written in a query string.
Parameters are checked on the source
The contract belongs to the operation a flow reads from, since that is the request being made. An operation used as a destination formats the write; its parameters are supplied by the flow, not by a caller.
Then in flows:
flow "list_active_users" {
from {
connector = "api"
operation = "GET /users"
}
to {
connector = "db"
operation = "find_active_users"
}
}
See the named-operations example for complete patterns.
Connector Profiles¶
A profiled connector is one name that resolves to a different backend at
runtime. Each profile declares what it is — its own type and driver —
so the alternatives do not have to be the same kind of thing: one flow can read
prices from an HTTP API for one tenant and from a database for another, without
knowing which it got.
Because the profile carries the type, a profiled connector has none at the root.
connector "prices" {
select = "env('PRICE_SOURCE')" # CEL expression evaluated per execution
default = "magento"
fallback = ["erp", "legacy"] # tried in order if the selected one fails
profile "magento" {
type = "http"
driver = "client"
base_url = env("MAGENTO_URL")
auth {
type = "bearer"
token = env("MAGENTO_TOKEN")
}
}
profile "erp" {
type = "database"
driver = "sqlite"
database = "erp.db"
}
}
The alternatives can equally be the same kind of backend — read replicas, a
tenant per database — in which case every profile repeats the same type and
driver and varies only the connection:
connector "db" {
select = "input.tenant_id"
default = "primary"
profile "primary" {
type = "database"
driver = "postgres"
host = env("PRIMARY_HOST")
database = "app"
}
profile "analytics" {
type = "database"
driver = "postgres"
host = env("ANALYTICS_HOST")
database = "app_analytics"
}
}
select is evaluated at flow execution time and its result names the profile;
default is used when it evaluates to nothing or names a profile that does not
exist. fallback lists profiles to try, in order, when the selected one fails.
See the profiles example for details.
TLS¶
Connectors that speak TLS — http, grpc, tcp, mq and mqtt — configure it
with the same block and the same attribute names.
connector "payments" {
type = "http"
base_url = "https://payments.internal"
tls {
ca_cert = "/certs/internal-ca.pem" # verify the other side
cert = "/certs/mycel.pem" # prove who we are (mutual TLS)
key = "/certs/mycel.key"
}
}
| Attribute | Type | Description |
|---|---|---|
enabled |
bool | Defaults to true when the block is present. Set it to false to switch TLS off without deleting the certificate paths, which is what makes it drivable from the environment. |
ca_cert |
string | CA certificate used to verify the other side. Needed for a private CA; the system trust store is used when it is absent. |
cert |
string | The certificate this connector presents — its own when it is a server, the client certificate for mutual TLS. |
key |
string | Private key for cert. |
server_name |
string | Expected server name, overriding the address used to connect (SNI). grpc only. |
insecure_skip_verify |
bool | Skip verification of the other side's certificate. Development only. |
Writing the block is the opt-in, so enabled = true is never required. This is
the same rule the mfa block follows.
cert and key are one setting seen from two sides: on a server they are the
certificate it presents, on a client the pair it uses for mutual TLS. The
connector already knows which it is, so the names do not repeat it.
A connector that cannot load its certificates does not start
If TLS is enabled and the certificate or CA cannot be read, startup fails with the reason. It is never downgraded to an unencrypted connection.
Older attribute names¶
Three connectors used to read different names for these settings. All of them are still accepted and mean exactly what they meant before, so existing configuration keeps working — but they are no longer offered as completions, and new configuration should use the names above.
| Older name | Written by | Now |
|---|---|---|
client_cert |
http |
cert |
client_key |
http |
key |
cert_file |
grpc |
cert |
key_file |
grpc |
key |
ca_file |
grpc |
ca_cert |
skip_verify |
grpc |
insecure_skip_verify |
Writing both spellings of one setting in the same block is an error rather than a silent choice between them.
Per-Connector Reference¶
For complete configuration options and examples for each connector type, see the Connector Catalog: